diff --git a/.gitignore b/.gitignore index 2c3156b9..fac00d41 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,8 @@ gha-creds-*.json # Log files patch_output.log + +# docs build +docs-site/.next +# content is a symlink to ../docs +docs-site/content diff --git a/.vscode/launch.json b/.vscode/launch.json index 0ae4f1b1..bab4f22e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,7 +27,7 @@ "outFiles": [ "${workspaceFolder}/packages/vscode-ide-companion/dist/**/*.js" ], - "preLaunchTask": "npm: build: vscode-ide-companion" + "preLaunchTask": "launch: vscode-ide-companion (copy+build)" }, { "name": "Attach", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 58709bc9..e0ee4730 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -20,6 +20,22 @@ "problemMatcher": [], "label": "npm: build: vscode-ide-companion", "detail": "npm run build -w packages/vscode-ide-companion" + }, + { + "label": "copy: bundled-cli (dev)", + "type": "shell", + "command": "node", + "args": ["packages/vscode-ide-companion/scripts/copy-bundled-cli.js"], + "problemMatcher": [] + }, + { + "label": "launch: vscode-ide-companion (copy+build)", + "dependsOrder": "sequence", + "dependsOn": [ + "copy: bundled-cli (dev)", + "npm: build: vscode-ide-companion" + ], + "problemMatcher": [] } ] } diff --git a/docs-site/README.md b/docs-site/README.md new file mode 100644 index 00000000..ad6272c3 --- /dev/null +++ b/docs-site/README.md @@ -0,0 +1,54 @@ +# Qwen Code Docs Site + +A documentation website for Qwen Code built with [Next.js](https://nextjs.org/) and [Nextra](https://nextra.site/). + +## Getting Started + +### Prerequisites + +- Node.js 18+ +- npm or yarn + +### Installation + +```bash +npm install +``` + +### Setup Content + +Link the documentation content from the parent `docs` directory: + +```bash +npm run link +``` + +This creates a symbolic link from `../docs` to `content` in the project. + +### Development + +Start the development server: + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser to see the documentation site. + +## Project Structure + +``` +docs-site/ +├── src/ +│ └── app/ +│ ├── [[...mdxPath]]/ # Dynamic routing for MDX pages +│ │ └── page.jsx +│ └── layout.jsx # Root layout with navbar and footer +├── mdx-components.js # MDX component configuration +├── next.config.mjs # Next.js configuration +└── package.json +``` + +## License + +MIT © Qwen Team diff --git a/docs-site/mdx-components.js b/docs-site/mdx-components.js new file mode 100644 index 00000000..ad856fe4 --- /dev/null +++ b/docs-site/mdx-components.js @@ -0,0 +1,12 @@ +import { useMDXComponents as getThemeComponents } from 'nextra-theme-docs'; // nextra-theme-blog or your custom theme + +// Get the default MDX components +const themeComponents = getThemeComponents(); + +// Merge components +export function useMDXComponents(components) { + return { + ...themeComponents, + ...components, + }; +} diff --git a/docs-site/next-env.d.ts b/docs-site/next-env.d.ts new file mode 100644 index 00000000..20e7bcfb --- /dev/null +++ b/docs-site/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import './.next/dev/types/routes.d.ts'; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/docs-site/next.config.mjs b/docs-site/next.config.mjs new file mode 100644 index 00000000..88adb868 --- /dev/null +++ b/docs-site/next.config.mjs @@ -0,0 +1,5 @@ +import nextra from 'nextra'; + +const withNextra = nextra({}); + +export default withNextra({}); diff --git a/docs-site/package.json b/docs-site/package.json new file mode 100644 index 00000000..1b5af5ae --- /dev/null +++ b/docs-site/package.json @@ -0,0 +1,22 @@ +{ + "name": "docs-site", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "index.js", + "scripts": { + "link": "ln -s ../docs content", + "clean": "rm -rf .next", + "dev": "npm run clean && next --turbopack", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "next": "^16.0.8", + "nextra": "^4.6.1", + "nextra-theme-docs": "^4.6.1", + "react": "^19.2.1", + "react-dom": "^19.2.1" + } +} diff --git a/docs-site/src/app/[[...mdxPath]]/page.jsx b/docs-site/src/app/[[...mdxPath]]/page.jsx new file mode 100644 index 00000000..c980e9f6 --- /dev/null +++ b/docs-site/src/app/[[...mdxPath]]/page.jsx @@ -0,0 +1,27 @@ +import { generateStaticParamsFor, importPage } from 'nextra/pages'; +import { useMDXComponents as getMDXComponents } from '../../../mdx-components'; + +export const generateStaticParams = generateStaticParamsFor('mdxPath'); + +export async function generateMetadata(props) { + const params = await props.params; + const { metadata } = await importPage(params.mdxPath); + return metadata; +} + +const Wrapper = getMDXComponents().wrapper; + +export default async function Page(props) { + const params = await props.params; + const { + default: MDXContent, + toc, + metadata, + sourceCode, + } = await importPage(params.mdxPath); + return ( + + + + ); +} diff --git a/docs-site/src/app/layout.jsx b/docs-site/src/app/layout.jsx new file mode 100644 index 00000000..87dd6c0c --- /dev/null +++ b/docs-site/src/app/layout.jsx @@ -0,0 +1,55 @@ +import { Footer, Layout, Navbar } from 'nextra-theme-docs'; +import { Banner, Head } from 'nextra/components'; +import { getPageMap } from 'nextra/page-map'; +import 'nextra-theme-docs/style.css'; + +export const metadata = { + // Define your metadata here + // For more information on metadata API, see: https://nextjs.org/docs/app/building-your-application/optimizing/metadata +}; + +const banner = ( + Qwen Code 0.5.0 is released 🎉 +); +const navbar = ( + Qwen Code} + // ... Your additional navbar options + /> +); +const footer =
MIT {new Date().getFullYear()} © Qwen Team.
; + +export default async function RootLayout({ children }) { + return ( + + + {/* Your additional tags should be passed as `children` of `` element */} + + + + {children} + + + + ); +} diff --git a/docs/_meta.ts b/docs/_meta.ts index 4939cb31..10f50a10 100644 --- a/docs/_meta.ts +++ b/docs/_meta.ts @@ -1,10 +1,14 @@ export default { - index: 'Welcome to Qwen Code', - cli: 'CLI', - core: 'Core', - tools: 'Tools', - features: 'Features', - 'ide-integration': 'IDE Integration', - development: 'Development', - support: 'Support', + index: { + type: 'page', + display: 'hidden', + }, + users: { + type: 'page', + title: 'User Guide', + }, + developers: { + type: 'page', + title: 'Developer Guide', + }, }; diff --git a/docs/assets/connected_devtools.png b/docs/assets/connected_devtools.png deleted file mode 100644 index 34a3c568..00000000 Binary files a/docs/assets/connected_devtools.png and /dev/null differ diff --git a/docs/assets/gemini-screenshot.png b/docs/assets/gemini-screenshot.png deleted file mode 100644 index 1cc163d8..00000000 Binary files a/docs/assets/gemini-screenshot.png and /dev/null differ diff --git a/docs/assets/qwen-screenshot.png b/docs/assets/qwen-screenshot.png deleted file mode 100644 index 15f66cce..00000000 Binary files a/docs/assets/qwen-screenshot.png and /dev/null differ diff --git a/docs/assets/release_patch.png b/docs/assets/release_patch.png deleted file mode 100644 index 952dc6ab..00000000 Binary files a/docs/assets/release_patch.png and /dev/null differ diff --git a/docs/assets/theme-ansi-light.png b/docs/assets/theme-ansi-light.png deleted file mode 100644 index 9766ae78..00000000 Binary files a/docs/assets/theme-ansi-light.png and /dev/null differ diff --git a/docs/assets/theme-ansi.png b/docs/assets/theme-ansi.png deleted file mode 100644 index 5d46daca..00000000 Binary files a/docs/assets/theme-ansi.png and /dev/null differ diff --git a/docs/assets/theme-atom-one.png b/docs/assets/theme-atom-one.png deleted file mode 100644 index c2787d6b..00000000 Binary files a/docs/assets/theme-atom-one.png and /dev/null differ diff --git a/docs/assets/theme-ayu-light.png b/docs/assets/theme-ayu-light.png deleted file mode 100644 index f1774656..00000000 Binary files a/docs/assets/theme-ayu-light.png and /dev/null differ diff --git a/docs/assets/theme-ayu.png b/docs/assets/theme-ayu.png deleted file mode 100644 index 99391f82..00000000 Binary files a/docs/assets/theme-ayu.png and /dev/null differ diff --git a/docs/assets/theme-custom.png b/docs/assets/theme-custom.png deleted file mode 100644 index 0eb80f96..00000000 Binary files a/docs/assets/theme-custom.png and /dev/null differ diff --git a/docs/assets/theme-default-light.png b/docs/assets/theme-default-light.png deleted file mode 100644 index 829d4ed5..00000000 Binary files a/docs/assets/theme-default-light.png and /dev/null differ diff --git a/docs/assets/theme-default.png b/docs/assets/theme-default.png deleted file mode 100644 index 0b93a334..00000000 Binary files a/docs/assets/theme-default.png and /dev/null differ diff --git a/docs/assets/theme-dracula.png b/docs/assets/theme-dracula.png deleted file mode 100644 index 27213fbc..00000000 Binary files a/docs/assets/theme-dracula.png and /dev/null differ diff --git a/docs/assets/theme-github-light.png b/docs/assets/theme-github-light.png deleted file mode 100644 index 3cdc94aa..00000000 Binary files a/docs/assets/theme-github-light.png and /dev/null differ diff --git a/docs/assets/theme-github.png b/docs/assets/theme-github.png deleted file mode 100644 index a62961b6..00000000 Binary files a/docs/assets/theme-github.png and /dev/null differ diff --git a/docs/assets/theme-google-light.png b/docs/assets/theme-google-light.png deleted file mode 100644 index 835ebc4b..00000000 Binary files a/docs/assets/theme-google-light.png and /dev/null differ diff --git a/docs/assets/theme-xcode-light.png b/docs/assets/theme-xcode-light.png deleted file mode 100644 index eb056a55..00000000 Binary files a/docs/assets/theme-xcode-light.png and /dev/null differ diff --git a/docs/cli/_meta.ts b/docs/cli/_meta.ts deleted file mode 100644 index 1557b595..00000000 --- a/docs/cli/_meta.ts +++ /dev/null @@ -1,35 +0,0 @@ -export default { - index: 'Introduction', - authentication: 'Authentication', - 'openai-auth': 'OpenAI Authentication', - commands: 'Commands', - configuration: 'Configuration', - 'configuration-v1': 'Configuration (v1)', - themes: 'Themes', - tutorials: 'Tutorials', - 'keyboard-shortcuts': 'Keyboard Shortcuts', - 'trusted-folders': 'Trusted Folders', - 'qwen-ignore': 'Ignoring Files', - Uninstall: 'Uninstall', -}; - -/** - * - * { "label": "Introduction", "slug": "docs/cli" }, - { "label": "Authentication", "slug": "docs/cli/authentication" }, - { "label": "Commands", "slug": "docs/cli/commands" }, - { "label": "Configuration", "slug": "docs/cli/configuration" }, - { "label": "Checkpointing", "slug": "docs/checkpointing" }, - { "label": "Extensions", "slug": "docs/extension" }, - { "label": "Headless Mode", "slug": "docs/headless" }, - { "label": "IDE Integration", "slug": "docs/ide-integration" }, - { - "label": "IDE Companion Spec", - "slug": "docs/ide-companion-spec" - }, - { "label": "Telemetry", "slug": "docs/telemetry" }, - { "label": "Themes", "slug": "docs/cli/themes" }, - { "label": "Token Caching", "slug": "docs/cli/token-caching" }, - { "label": "Trusted Folders", "slug": "docs/trusted-folders" }, - { "label": "Tutorials", "slug": "docs/cli/tutorials" } - */ diff --git a/docs/cli/authentication.md b/docs/cli/authentication.md deleted file mode 100644 index 43f55d53..00000000 --- a/docs/cli/authentication.md +++ /dev/null @@ -1,137 +0,0 @@ -# Authentication Setup - -Qwen Code supports two main authentication methods to access AI models. Choose the method that best fits your use case: - -1. **Qwen OAuth (Recommended):** - - Use this option to log in with your qwen.ai account. - - During initial startup, Qwen Code will direct you to the qwen.ai authentication page. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs. - - **Requirements:** - - Valid qwen.ai account - - Internet connection for initial authentication - - **Benefits:** - - Seamless access to Qwen models - - Automatic credential refresh - - No manual API key management required - - **Getting Started:** - - ```bash - # Start Qwen Code and follow the OAuth flow - qwen - ``` - - The CLI will automatically open your browser and guide you through the authentication process. - - **For users who authenticate using their qwen.ai account:** - - **Quota:** - - 60 requests per minute - - 2,000 requests per day - - Token usage is not applicable - - **Cost:** Free - - **Notes:** A specific quota for different models is not specified; model fallback may occur to preserve shared experience quality. - -2. **OpenAI-Compatible API:** - - Use API keys for OpenAI or other compatible providers. - - This method allows you to use various AI models through API keys. - - **Configuration Methods:** - - a) **Environment Variables:** - - ```bash - export OPENAI_API_KEY="your_api_key_here" - export OPENAI_BASE_URL="your_api_endpoint" # Optional - export OPENAI_MODEL="your_model_choice" # Optional - ``` - - b) **Project `.env` File:** - Create a `.env` file in your project root: - - ```env - OPENAI_API_KEY=your_api_key_here - OPENAI_BASE_URL=your_api_endpoint - OPENAI_MODEL=your_model_choice - ``` - - **Supported Providers:** - - OpenAI (https://platform.openai.com/api-keys) - - Alibaba Cloud Bailian - - ModelScope - - OpenRouter - - Azure OpenAI - - Any OpenAI-compatible API - -## Switching Authentication Methods - -To switch between authentication methods during a session, use the `/auth` command in the CLI interface: - -```bash -# Within the CLI, type: -/auth -``` - -This will allow you to reconfigure your authentication method without restarting the application. - -### Persisting Environment Variables with `.env` Files - -You can create a **`.qwen/.env`** file in your project directory or in your home directory. Creating a plain **`.env`** file also works, but `.qwen/.env` is recommended to keep Qwen Code variables isolated from other tools. - -**Important:** Some environment variables (like `DEBUG` and `DEBUG_MODE`) are automatically excluded from project `.env` files to prevent interference with qwen-code behavior. Use `.qwen/.env` files for qwen-code specific variables. - -Qwen Code automatically loads environment variables from the **first** `.env` file it finds, using the following search order: - -1. Starting in the **current directory** and moving upward toward `/`, for each directory it checks: - 1. `.qwen/.env` - 2. `.env` -2. If no file is found, it falls back to your **home directory**: - - `~/.qwen/.env` - - `~/.env` - -> **Important:** The search stops at the **first** file encountered—variables are **not merged** across multiple files. - -#### Examples - -**Project-specific overrides** (take precedence when you are inside the project): - -```bash -mkdir -p .qwen -cat >> .qwen/.env <<'EOF' -OPENAI_API_KEY="your-api-key" -OPENAI_BASE_URL="https://api-inference.modelscope.cn/v1" -OPENAI_MODEL="Qwen/Qwen3-Coder-480B-A35B-Instruct" -EOF -``` - -**User-wide settings** (available in every directory): - -```bash -mkdir -p ~/.qwen -cat >> ~/.qwen/.env <<'EOF' -OPENAI_API_KEY="your-api-key" -OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" -OPENAI_MODEL="qwen3-coder-plus" -EOF -``` - -## Non-Interactive Mode / Headless Environments - -When running Qwen Code in a non-interactive environment, you cannot use the OAuth login flow. -Instead, you must configure authentication using environment variables. - -The CLI will automatically detect if it is running in a non-interactive terminal and will use the -OpenAI-compatible API method if configured: - -1. **OpenAI-Compatible API:** - - Set the `OPENAI_API_KEY` environment variable. - - Optionally set `OPENAI_BASE_URL` and `OPENAI_MODEL` for custom endpoints. - - The CLI will use these credentials to authenticate with the API provider. - -**Example for headless environments:** - -If none of these environment variables are set in a non-interactive session, the CLI will exit with an error. - -For comprehensive guidance on using Qwen COde programmatically and in -automation workflows, see the [Headless Mode Guide](../headless.md). diff --git a/docs/cli/commands.md b/docs/cli/commands.md deleted file mode 100644 index aa056a43..00000000 --- a/docs/cli/commands.md +++ /dev/null @@ -1,475 +0,0 @@ -# CLI Commands - -Qwen Code supports several built-in commands to help you manage your session, customize the interface, and control its behavior. These commands are prefixed with a forward slash (`/`), an at symbol (`@`), or an exclamation mark (`!`). - -## Slash commands (`/`) - -Slash commands provide meta-level control over the CLI itself. - -### Built-in Commands - -- **`/bug`** - - **Description:** File an issue about Qwen Code. By default, the issue is filed within the GitHub repository for Qwen Code. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `advanced.bugCommand` setting in your `.qwen/settings.json` files. - -- **`/clear`** (aliases: `reset`, `new`) - - **Description:** Clear conversation history and free up context by starting a fresh session. Also clears the terminal output and scrollback within the CLI. - - **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action. - -- **`/summary`** - - **Description:** Generate a comprehensive project summary from the current conversation history and save it to `.qwen/PROJECT_SUMMARY.md`. This summary includes the overall goal, key knowledge, recent actions, and current plan, making it perfect for resuming work in future sessions. - - **Usage:** `/summary` - - **Features:** - - Analyzes the entire conversation history to extract important context - - Creates a structured markdown summary with sections for goals, knowledge, actions, and plans - - Automatically saves to `.qwen/PROJECT_SUMMARY.md` in your project root - - Shows progress indicators during generation and saving - - Integrates with the Welcome Back feature for seamless session resumption - - **Note:** This command requires an active conversation with at least 2 messages to generate a meaningful summary. - -- **`/compress`** - - **Description:** Replace the entire chat context with a summary. This saves on tokens used for future tasks while retaining a high level summary of what has happened. - -- **`/copy`** - - **Description:** Copies the last output produced by Qwen Code to your clipboard, for easy sharing or reuse. - -- **`/directory`** (or **`/dir`**) - - **Description:** Manage workspace directories for multi-directory support. - - **Sub-commands:** - - **`add`**: - - **Description:** Add a directory to the workspace. The path can be absolute or relative to the current working directory. Moreover, the reference from home directory is supported as well. - - **Usage:** `/directory add ,` - - **Note:** Disabled in restrictive sandbox profiles. If you're using that, use `--include-directories` when starting the session instead. - - **`show`**: - - **Description:** Display all directories added by `/directory add` and `--include-directories`. - - **Usage:** `/directory show` - -- **`/editor`** - - **Description:** Open a dialog for selecting supported editors. - -- **`/extensions`** - - **Description:** Lists all active extensions in the current Qwen Code session. See [Qwen Code Extensions](../extension.md). - -- **`/help`** (or **`/?`**) - - **Description:** Display help information about the Qwen Code, including available commands and their usage. - -- **`/mcp`** - - **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools. - - **Sub-commands:** - - **`desc`** or **`descriptions`**: - - **Description:** Show detailed descriptions for MCP servers and tools. - - **`nodesc`** or **`nodescriptions`**: - - **Description:** Hide tool descriptions, showing only the tool names. - - **`schema`**: - - **Description:** Show the full JSON schema for the tool's configured parameters. - - **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between showing and hiding tool descriptions. - -- **`/memory`** - - **Description:** Manage the AI's instructional context (hierarchical memory loaded from `QWEN.md` files by default; configurable via `contextFileName`). - - **Sub-commands:** - - **`add`**: - - **Description:** Adds the following text to the AI's memory. Usage: `/memory add ` - - **`show`**: - - **Description:** Display the full, concatenated content of the current hierarchical memory that has been loaded from all context files (e.g., `QWEN.md`). This lets you inspect the instructional context being provided to the model. - - **`refresh`**: - - **Description:** Reload the hierarchical instructional memory from all context files (default: `QWEN.md`) found in the configured locations (global, project/ancestors, and sub-directories). This updates the model with the latest context content. - - **Note:** For more details on how context files contribute to hierarchical memory, see the [CLI Configuration documentation](./configuration.md#context-files-hierarchical-instructional-context). - -- **`/model`** - - **Description:** Switch the model for the current session. Opens a dialog to select from available models based on your authentication type. - - **Usage:** `/model` - - **Features:** - - Shows a dialog with all available models for your current authentication type - - Displays model descriptions and capabilities (e.g., vision support) - - Changes the model for the current session only - - Supports both Qwen models (via OAuth) and OpenAI models (via API key) - - **Available Models:** - - **Qwen Coder:** The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23) - - **Qwen Vision:** The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23) - supports image analysis - - **OpenAI Models:** Available when using OpenAI authentication (configured via `OPENAI_MODEL` environment variable) - - **Note:** Model selection is session-specific and does not persist across different Qwen Code sessions. To set a default model, use the `model.name` setting in your configuration. - -- **`/restore`** - - **Description:** Restores the project files to the state they were in just before a tool was executed. This is particularly useful for undoing file edits made by a tool. If run without a tool call ID, it will list available checkpoints to restore from. - - **Usage:** `/restore [tool_call_id]` - - **Note:** Only available if the CLI is invoked with the `--checkpointing` option or configured via [settings](./configuration.md). See [Checkpointing documentation](../checkpointing.md) for more details. - -- **`/settings`** - - **Description:** Open the settings editor to view and modify Qwen Code settings. - - **Details:** This command provides a user-friendly interface for changing settings that control the behavior and appearance of Qwen Code. It is equivalent to manually editing the `.qwen/settings.json` file, but with validation and guidance to prevent errors. - - **Usage:** Simply run `/settings` and the editor will open. You can then browse or search for specific settings, view their current values, and modify them as desired. Changes to some settings are applied immediately, while others require a restart. - -- **`/stats`** - - **Description:** Display detailed statistics for the current Qwen Code session, including token usage, cached token savings (when available), and session duration. Note: Cached token information is only displayed when cached tokens are being used, which occurs with API key authentication but not with OAuth authentication at this time. - -- [**`/theme`**](./themes.md) - - **Description:** Open a dialog that lets you change the visual theme of Qwen Code. - -- **`/auth`** - - **Description:** Open a dialog that lets you change the authentication method. - -- **`/approval-mode`** - - **Description:** Change the approval mode for tool usage. - - **Usage:** `/approval-mode [mode] [--session|--project|--user]` - - **Available Modes:** - - **`plan`**: Analyze only; do not modify files or execute commands - - **`default`**: Require approval for file edits or shell commands - - **`auto-edit`**: Automatically approve file edits - - **`yolo`**: Automatically approve all tools - - **Examples:** - - `/approval-mode plan --project` (persist plan mode for this project) - - `/approval-mode yolo --user` (persist YOLO mode for this user across projects) - -- **`/about`** - - **Description:** Show version info. Please share this information when filing issues. - -- **`/agents`** - - **Description:** Manage specialized AI subagents for focused tasks. Subagents are independent AI assistants configured with specific expertise and tool access. - - **Sub-commands:** - - **`create`**: - - **Description:** Launch an interactive wizard to create a new subagent. The wizard guides you through location selection, AI-powered prompt generation, tool selection, and visual customization. - - **Usage:** `/agents create` - - **`manage`**: - - **Description:** Open an interactive management dialog to view, edit, and delete existing subagents. Shows both project-level and user-level agents. - - **Usage:** `/agents manage` - - **Storage Locations:** - - **Project-level:** `.qwen/agents/` (shared with team, takes precedence) - - **User-level:** `~/.qwen/agents/` (personal agents, available across projects) - - **Note:** For detailed information on creating and managing subagents, see the [Subagents documentation](../subagents.md). - -- [**`/tools`**](../tools/index.md) - - **Description:** Display a list of tools that are currently available within Qwen Code. - - **Usage:** `/tools [desc]` - - **Sub-commands:** - - **`desc`** or **`descriptions`**: - - **Description:** Show detailed descriptions of each tool, including each tool's name with its full description as provided to the model. - - **`nodesc`** or **`nodescriptions`**: - - **Description:** Hide tool descriptions, showing only the tool names. - -- **`/quit`** (or **`/exit`**) - - **Description:** Exit Qwen Code immediately without any confirmation dialog. - -- **`/vim`** - - **Description:** Toggle vim mode on or off. When vim mode is enabled, the input area supports vim-style navigation and editing commands in both NORMAL and INSERT modes. - - **Features:** - - **NORMAL mode:** Navigate with `h`, `j`, `k`, `l`; jump by words with `w`, `b`, `e`; go to line start/end with `0`, `$`, `^`; go to specific lines with `G` (or `gg` for first line) - - **INSERT mode:** Standard text input with escape to return to NORMAL mode - - **Editing commands:** Delete with `x`, change with `c`, insert with `i`, `a`, `o`, `O`; complex operations like `dd`, `cc`, `dw`, `cw` - - **Count support:** Prefix commands with numbers (e.g., `3h`, `5w`, `10G`) - - **Repeat last command:** Use `.` to repeat the last editing operation - - **Persistent setting:** Vim mode preference is saved to `~/.qwen/settings.json` and restored between sessions - - **Status indicator:** When enabled, shows `[NORMAL]` or `[INSERT]` in the footer - -- **`/init`** - - **Description:** Analyzes the current directory and creates a `QWEN.md` context file by default (or the filename specified by `contextFileName`). If a non-empty file already exists, no changes are made. The command seeds an empty file and prompts the model to populate it with project-specific instructions. - -- [**`/language`**](./language.md) - - **Description:** View or change the language setting for both UI and LLM output. - - **Sub-commands:** - - **`ui`**: Set the UI language (zh-CN or en-US) - - **`output`**: Set the LLM output language - - **Usage:** `/language [ui|output] [language]` - - **Examples:** - - `/language ui zh-CN` (set UI language to Simplified Chinese) - - `/language output English` (set LLM output language to English) - -### Custom Commands - -For a quick start, see the [example](#example-a-pure-function-refactoring-command) below. - -Custom commands allow you to save and reuse your favorite or most frequently used prompts as personal shortcuts within Qwen Code. You can create commands that are specific to a single project or commands that are available globally across all your projects, streamlining your workflow and ensuring consistency. - -#### File Locations & Precedence - -Qwen Code discovers commands from two locations, loaded in a specific order: - -1. **User Commands (Global):** Located in `~/.qwen/commands/`. These commands are available in any project you are working on. -2. **Project Commands (Local):** Located in `/.qwen/commands/`. These commands are specific to the current project and can be checked into version control to be shared with your team. - -If a command in the project directory has the same name as a command in the user directory, the **project command will always be used.** This allows projects to override global commands with project-specific versions. - -#### Naming and Namespacing - -The name of a command is determined by its file path relative to its `commands` directory. Subdirectories are used to create namespaced commands, with the path separator (`/` or `\`) being converted to a colon (`:`). - -- A file at `~/.qwen/commands/test.toml` becomes the command `/test`. -- A file at `/.qwen/commands/git/commit.toml` becomes the namespaced command `/git:commit`. - -#### TOML File Format (v1) - -Your command definition files must be written in the TOML format and use the `.toml` file extension. - -##### Required Fields - -- `prompt` (String): The prompt that will be sent to the model when the command is executed. This can be a single-line or multi-line string. - -##### Optional Fields - -- `description` (String): A brief, one-line description of what the command does. This text will be displayed next to your command in the `/help` menu. **If you omit this field, a generic description will be generated from the filename.** - -#### Handling Arguments - -Custom commands support two powerful methods for handling arguments. The CLI automatically chooses the correct method based on the content of your command's `prompt`. - -##### 1. Context-Aware Injection with `{{args}}` - -If your `prompt` contains the special placeholder `{{args}}`, the CLI will replace that placeholder with the text the user typed after the command name. - -The behavior of this injection depends on where it is used: - -**A. Raw Injection (Outside Shell Commands)** - -When used in the main body of the prompt, the arguments are injected exactly as the user typed them. - -**Example (`git/fix.toml`):** - -```toml -# Invoked via: /git:fix "Button is misaligned" - -description = "Generates a fix for a given issue." -prompt = "Please provide a code fix for the issue described here: {{args}}." -``` - -The model receives: `Please provide a code fix for the issue described here: "Button is misaligned".` - -**B. Using Arguments in Shell Commands (Inside `!{...}` Blocks)** - -When you use `{{args}}` inside a shell injection block (`!{...}`), the arguments are automatically **shell-escaped** before replacement. This allows you to safely pass arguments to shell commands, ensuring the resulting command is syntactically correct and secure while preventing command injection vulnerabilities. - -**Example (`/grep-code.toml`):** - -```toml -prompt = """ -Please summarize the findings for the pattern `{{args}}`. - -Search Results: -!{grep -r {{args}} .} -""" -``` - -When you run `/grep-code It's complicated`: - -1. The CLI sees `{{args}}` used both outside and inside `!{...}`. -2. Outside: The first `{{args}}` is replaced raw with `It's complicated`. -3. Inside: The second `{{args}}` is replaced with the escaped version (e.g., on Linux: `"It's complicated"`). -4. The command executed is `grep -r "It's complicated" .`. -5. The CLI prompts you to confirm this exact, secure command before execution. -6. The final prompt is sent. - -##### 2. Default Argument Handling - -If your `prompt` does **not** contain the special placeholder `{{args}}`, the CLI uses a default behavior for handling arguments. - -If you provide arguments to the command (e.g., `/mycommand arg1`), the CLI will append the full command you typed to the end of the prompt, separated by two newlines. This allows the model to see both the original instructions and the specific arguments you just provided. - -If you do **not** provide any arguments (e.g., `/mycommand`), the prompt is sent to the model exactly as it is, with nothing appended. - -**Example (`changelog.toml`):** - -This example shows how to create a robust command by defining a role for the model, explaining where to find the user's input, and specifying the expected format and behavior. - -```toml -# In: /.qwen/commands/changelog.toml -# Invoked via: /changelog 1.2.0 added "Support for default argument parsing." - -description = "Adds a new entry to the project's CHANGELOG.md file." -prompt = """ -# Task: Update Changelog - -You are an expert maintainer of this software project. A user has invoked a command to add a new entry to the changelog. - -**The user's raw command is appended below your instructions.** - -Your task is to parse the ``, ``, and `` from their input and use the `write_file` tool to correctly update the `CHANGELOG.md` file. - -## Expected Format -The command follows this format: `/changelog ` -- `` must be one of: "added", "changed", "fixed", "removed". - -## Behavior -1. Read the `CHANGELOG.md` file. -2. Find the section for the specified ``. -3. Add the `` under the correct `` heading. -4. If the version or type section doesn't exist, create it. -5. Adhere strictly to the "Keep a Changelog" format. -""" -``` - -When you run `/changelog 1.2.0 added "New feature"`, the final text sent to the model will be the original prompt followed by two newlines and the command you typed. - -##### 3. Executing Shell Commands with `!{...}` - -You can make your commands dynamic by executing shell commands directly within your `prompt` and injecting their output. This is ideal for gathering context from your local environment, like reading file content or checking the status of Git. - -When a custom command attempts to execute a shell command, Qwen Code will now prompt you for confirmation before proceeding. This is a security measure to ensure that only intended commands can be run. - -**How It Works:** - -1. **Inject Commands:** Use the `!{...}` syntax. -2. **Argument Substitution:** If `{{args}}` is present inside the block, it is automatically shell-escaped (see [Context-Aware Injection](#1-context-aware-injection-with-args) above). -3. **Robust Parsing:** The parser correctly handles complex shell commands that include nested braces, such as JSON payloads. **Note:** The content inside `!{...}` must have balanced braces (`{` and `}`). If you need to execute a command containing unbalanced braces, consider wrapping it in an external script file and calling the script within the `!{...}` block. -4. **Security Check and Confirmation:** The CLI performs a security check on the final, resolved command (after arguments are escaped and substituted). A dialog will appear showing the exact command(s) to be executed. -5. **Execution and Error Reporting:** The command is executed. If the command fails, the output injected into the prompt will include the error messages (stderr) followed by a status line, e.g., `[Shell command exited with code 1]`. This helps the model understand the context of the failure. - -**Example (`git/commit.toml`):** - -This command gets the staged git diff and uses it to ask the model to write a commit message. - -````toml -# In: /.qwen/commands/git/commit.toml -# Invoked via: /git:commit - -description = "Generates a Git commit message based on staged changes." - -# The prompt uses !{...} to execute the command and inject its output. -prompt = """ -Please generate a Conventional Commit message based on the following git diff: - -```diff -!{git diff --staged} -``` - -""" - -```` - -When you run `/git:commit`, the CLI first executes `git diff --staged`, then replaces `!{git diff --staged}` with the output of that command before sending the final, complete prompt to the model. - -##### 4. Injecting File Content with `@{...}` - -You can directly embed the content of a file or a directory listing into your prompt using the `@{...}` syntax. This is useful for creating commands that operate on specific files. - -**How It Works:** - -- **File Injection**: `@{path/to/file.txt}` is replaced by the content of `file.txt`. -- **Multimodal Support**: If the path points to a supported image (e.g., PNG, JPEG), PDF, audio, or video file, it will be correctly encoded and injected as multimodal input. Other binary files are handled gracefully and skipped. -- **Directory Listing**: `@{path/to/dir}` is traversed and each file present within the directory and all subdirectories are inserted into the prompt. This respects `.gitignore` and `.qwenignore` if enabled. -- **Workspace-Aware**: The command searches for the path in the current directory and any other workspace directories. Absolute paths are allowed if they are within the workspace. -- **Processing Order**: File content injection with `@{...}` is processed _before_ shell commands (`!{...}`) and argument substitution (`{{args}}`). -- **Parsing**: The parser requires the content inside `@{...}` (the path) to have balanced braces (`{` and `}`). - -**Example (`review.toml`):** - -This command injects the content of a _fixed_ best practices file (`docs/best-practices.md`) and uses the user's arguments to provide context for the review. - -```toml -# In: /.qwen/commands/review.toml -# Invoked via: /review FileCommandLoader.ts - -description = "Reviews the provided context using a best practice guide." -prompt = """ -You are an expert code reviewer. - -Your task is to review {{args}}. - -Use the following best practices when providing your review: - -@{docs/best-practices.md} -""" -``` - -When you run `/review FileCommandLoader.ts`, the `@{docs/best-practices.md}` placeholder is replaced by the content of that file, and `{{args}}` is replaced by the text you provided, before the final prompt is sent to the model. - ---- - -#### Example: A "Pure Function" Refactoring Command - -Let's create a global command that asks the model to refactor a piece of code. - -**1. Create the file and directories:** - -First, ensure the user commands directory exists, then create a `refactor` subdirectory for organization and the final TOML file. - -```bash -mkdir -p ~/.qwen/commands/refactor -touch ~/.qwen/commands/refactor/pure.toml -``` - -**2. Add the content to the file:** - -Open `~/.qwen/commands/refactor/pure.toml` in your editor and add the following content. We are including the optional `description` for best practice. - -```toml -# In: ~/.qwen/commands/refactor/pure.toml -# This command will be invoked via: /refactor:pure - -description = "Asks the model to refactor the current context into a pure function." - -prompt = """ -Please analyze the code I've provided in the current context. -Refactor it into a pure function. - -Your response should include: -1. The refactored, pure function code block. -2. A brief explanation of the key changes you made and why they contribute to purity. -""" -``` - -**3. Run the Command:** - -That's it! You can now run your command in the CLI. First, you might add a file to the context, and then invoke your command: - -``` -> @my-messy-function.js -> /refactor:pure -``` - -Qwen Code will then execute the multi-line prompt defined in your TOML file. - -## Input Prompt Shortcuts - -These shortcuts apply directly to the input prompt for text manipulation. - -- **Undo:** - - **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input prompt. - -- **Redo:** - - **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action in the input prompt. - -## At commands (`@`) - -At commands are used to include the content of files or directories as part of your prompt to the model. These commands include git-aware filtering. - -- **`@`** - - **Description:** Inject the content of the specified file or files into your current prompt. This is useful for asking questions about specific code, text, or collections of files. - - **Examples:** - - `@path/to/your/file.txt Explain this text.` - - `@src/my_project/ Summarize the code in this directory.` - - `What is this file about? @README.md` - - **Details:** - - If a path to a single file is provided, the content of that file is read. - - If a path to a directory is provided, the command attempts to read the content of files within that directory and any subdirectories. - - Spaces in paths should be escaped with a backslash (e.g., `@My\ Documents/file.txt`). - - The command uses the `read_many_files` tool internally. The content is fetched and then inserted into your query before being sent to the model. - - **Git-aware filtering:** By default, git-ignored files (like `node_modules/`, `dist/`, `.env`, `.git/`) are excluded. This behavior can be changed via the `context.fileFiltering` settings. - - **File types:** The command is intended for text-based files. While it might attempt to read any file, binary files or very large files might be skipped or truncated by the underlying `read_many_files` tool to ensure performance and relevance. The tool indicates if files were skipped. - - **Output:** The CLI will show a tool call message indicating that `read_many_files` was used, along with a message detailing the status and the path(s) that were processed. - -- **`@` (Lone at symbol)** - - **Description:** If you type a lone `@` symbol without a path, the query is passed as-is to the model. This might be useful if you are specifically talking _about_ the `@` symbol in your prompt. - -### Error handling for `@` commands - -- If the path specified after `@` is not found or is invalid, an error message will be displayed, and the query might not be sent to the model, or it will be sent without the file content. -- If the `read_many_files` tool encounters an error (e.g., permission issues), this will also be reported. - -## Shell mode & passthrough commands (`!`) - -The `!` prefix lets you interact with your system's shell directly from within Qwen Code. - -- **`!`** - - **Description:** Execute the given `` using `bash` on Linux/macOS or `cmd.exe` on Windows. Any output or errors from the command are displayed in the terminal. - - **Examples:** - - `!ls -la` (executes `ls -la` and returns to Qwen Code) - - `!git status` (executes `git status` and returns to Qwen Code) - -- **`!` (Toggle shell mode)** - - **Description:** Typing `!` on its own toggles shell mode. - - **Entering shell mode:** - - When active, shell mode uses a different coloring and a "Shell Mode Indicator". - - While in shell mode, text you type is interpreted directly as a shell command. - - **Exiting shell mode:** - - When exited, the UI reverts to its standard appearance and normal Qwen Code behavior resumes. - -- **Caution for all `!` usage:** Commands you execute in shell mode have the same permissions and impact as if you ran them directly in your terminal. - -- **Environment Variable:** When a command is executed via `!` or in shell mode, the `QWEN_CODE=1` environment variable is set in the subprocess's environment. This allows scripts or tools to detect if they are being run from within the CLI. diff --git a/docs/cli/configuration-v1.md b/docs/cli/configuration-v1.md deleted file mode 100644 index 2037db8d..00000000 --- a/docs/cli/configuration-v1.md +++ /dev/null @@ -1,674 +0,0 @@ -# Qwen Code Configuration - -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): - -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: - -- **System defaults file:** - - **Location:** `/etc/qwen-code/system-defaults.json` (Linux), `C:\ProgramData\qwen-code\system-defaults.json` (Windows) or `/Library/Application Support/QwenCode/system-defaults.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. - - **Scope:** 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:** - - **Location:** `~/.qwen/settings.json` (where `~` is your home directory). - - **Scope:** Applies to all Qwen Code sessions for the current user. -- **Project settings file:** - - **Location:** `.qwen/settings.json` within your project's root directory. - - **Scope:** Applies only when running Qwen Code from that specific project. Project settings override user settings. - -- **System settings file:** - - **Location:** `/etc/qwen-code/settings.json` (Linux), `C:\ProgramData\qwen-code\settings.json` (Windows) or `/Library/Application Support/QwenCode/settings.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. - - **Scope:** 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 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](#sandboxing) (e.g., `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`). - -### Available settings in `settings.json`: - -- **`contextFileName`** (string or array of strings): - - **Description:** Specifies the filename for context files (e.g., `QWEN.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames. - - **Default:** `QWEN.md` - - **Example:** `"contextFileName": "AGENTS.md"` - -- **`bugCommand`** (object): - - **Description:** Overrides the default URL for the `/bug` command. - - **Default:** `"urlTemplate": "https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title={title}&info={info}"` - - **Properties:** - - **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}` placeholders. - - **Example:** - ```json - "bugCommand": { - "urlTemplate": "https://bug.example.com/new?title={title}&info={info}" - } - ``` - -- **`fileFiltering`** (object): - - **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools. - - **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true` - - **Properties:** - - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations. - - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt. - - **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files. - - **Example:** - ```json - "fileFiltering": { - "respectGitIgnore": true, - "enableRecursiveFileSearch": false, - "disableFuzzySearch": true - } - ``` - -### 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 `disableFuzzySearch` to `true` 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. - -- **`coreTools`** (array of strings): - - **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"coreTools": ["ShellTool(ls -l)"]` will only allow the `ls -l` command to be executed. - - **Default:** All tools available for use by the model. - - **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`. - -- **`allowedTools`** (array of strings): - - **Default:** `undefined` - - **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. The match semantics are the same as `coreTools`. - - **Example:** `"allowedTools": ["ShellTool(git status)"]`. - -- **`excludeTools`** (array of strings): - - **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded. You can also specify command-specific restrictions for tools that support it, like the `ShellTool`. For example, `"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command. - - **Default**: No tools excluded. - - **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`. - - **Security Note:** Command-specific restrictions in - `excludeTools` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `coreTools` to explicitly select commands - that can be executed. - -- **`allowMCPServers`** (array of strings): - - **Description:** 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. - - **Default:** All MCP servers are available for use by the model. - - **Example:** `"allowMCPServers": ["myPythonServer"]`. - - **Security Note:** This uses 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. - -- **`excludeMCPServers`** (array of strings): - - **Description:** Allows you to specify a list of MCP server names that should be excluded from the model. A server listed in both `excludeMCPServers` and `allowMCPServers` is excluded. Note that this will be ignored if `--allowed-mcp-server-names` is set. - - **Default**: No MCP servers excluded. - - **Example:** `"excludeMCPServers": ["myNodeServer"]`. - - **Security Note:** This uses 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. - -- **`autoAccept`** (boolean): - - **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. - - **Default:** `false` - - **Example:** `"autoAccept": true` - -- **`theme`** (string): - - **Description:** Sets the visual [theme](./themes.md) for Qwen Code. - - **Default:** `"Default"` - - **Example:** `"theme": "GitHub"` - -- **`vimMode`** (boolean): - - **Description:** Enables or disables vim mode for input editing. When enabled, the input area supports vim-style navigation and editing commands with NORMAL and INSERT modes. The vim mode status is displayed in the footer and persists between sessions. - - **Default:** `false` - - **Example:** `"vimMode": true` - -- **`sandbox`** (boolean or string): - - **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Qwen Code uses a pre-built `qwen-code-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing). - - **Default:** `false` - - **Example:** `"sandbox": "docker"` - -- **`toolDiscoveryCommand`** (string): - - **Description:** **Align with Gemini CLI.** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional. - - **Default:** Empty - - **Example:** `"toolDiscoveryCommand": "bin/get_tools"` - -- **`toolCallCommand`** (string): - - **Description:** **Align with Gemini CLI.** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. 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). - - **Default:** Empty - - **Example:** `"toolCallCommand": "bin/call_tool"` - -- **`mcpServers`** (object): - - **Description:** 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`. - - **Default:** Empty - - **Properties:** - - **``** (object): The server parameters for the named server. - - `command` (string, optional): The command to execute to start the MCP server via standard I/O. - - `args` (array of strings, optional): Arguments to pass to the command. - - `env` (object, optional): Environment variables to set for the server process. - - `cwd` (string, optional): The working directory in which to start the server. - - `url` (string, optional): The URL of an MCP server that uses Server-Sent Events (SSE) for communication. - - `httpUrl` (string, optional): The URL of an MCP server that uses streamable HTTP for communication. - - `headers` (object, optional): A map of HTTP headers to send with requests to `url` or `httpUrl`. - - `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server. - - `trust` (boolean, optional): Trust this server and bypass all tool call confirmations. - - `description` (string, optional): A brief description of the server, which may be used for display purposes. - - `includeTools` (array of strings, optional): List of tool names to include from this MCP server. When specified, only the tools listed here will be available from this server (whitelist behavior). If not specified, all tools from the server are enabled by default. - - `excludeTools` (array of strings, optional): 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. - - **Example:** - ```json - "mcpServers": { - "myPythonServer": { - "command": "python", - "args": ["mcp_server.py", "--port", "8080"], - "cwd": "./mcp_tools/python", - "timeout": 5000, - "includeTools": ["safe_tool", "file_reader"], - }, - "myNodeServer": { - "command": "node", - "args": ["mcp_server.js"], - "cwd": "./mcp_tools/node", - "excludeTools": ["dangerous_tool", "file_deleter"] - }, - "myDockerServer": { - "command": "docker", - "args": ["run", "-i", "--rm", "-e", "API_KEY", "ghcr.io/foo/bar"], - "env": { - "API_KEY": "$MY_API_TOKEN" - } - }, - "mySseServer": { - "url": "http://localhost:8081/events", - "headers": { - "Authorization": "Bearer $MY_SSE_TOKEN" - }, - "description": "An example SSE-based MCP server." - }, - "myStreamableHttpServer": { - "httpUrl": "http://localhost:8082/stream", - "headers": { - "X-API-Key": "$MY_HTTP_API_KEY" - }, - "description": "An example Streamable HTTP-based MCP server." - } - } - ``` - -- **`checkpointing`** (object): - - **Description:** Configures the checkpointing feature, which allows you to save and restore conversation and file states. See the [Checkpointing documentation](../checkpointing.md) for more details. - - **Default:** `{"enabled": false}` - - **Properties:** - - **`enabled`** (boolean): When `true`, the `/restore` command is available. - -- **`preferredEditor`** (string): - - **Description:** Specifies the preferred editor to use for viewing diffs. - - **Default:** `vscode` - - **Example:** `"preferredEditor": "vscode"` - -- **`telemetry`** (object) - - **Description:** Configures logging and metrics collection for Qwen Code. For more information, see [Telemetry](../telemetry.md). - - **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}` - - **Properties:** - - **`enabled`** (boolean): Whether or not telemetry is enabled. - - **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`. - - **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter. - - **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs. - - **Example:** - ```json - "telemetry": { - "enabled": true, - "target": "local", - "otlpEndpoint": "http://localhost:16686", - "logPrompts": false - } - ``` -- **`usageStatisticsEnabled`** (boolean): - - **Description:** Enables or disables the collection of usage statistics. See [Usage Statistics](#usage-statistics) for more information. - - **Default:** `true` - - **Example:** - ```json - "usageStatisticsEnabled": false - ``` - -- **`hideTips`** (boolean): - - **Description:** Enables or disables helpful tips in the CLI interface. - - **Default:** `false` - - **Example:** - - ```json - "hideTips": true - ``` - -- **`hideBanner`** (boolean): - - **Description:** Enables or disables the startup banner (ASCII art logo) in the CLI interface. - - **Default:** `false` - - **Example:** - - ```json - "hideBanner": true - ``` - -- **`maxSessionTurns`** (number): - - **Description:** Sets the maximum number of turns for a session. If the session exceeds this limit, the CLI will stop processing and start a new chat. - - **Default:** `-1` (unlimited) - - **Example:** - ```json - "maxSessionTurns": 10 - ``` - -- **`summarizeToolOutput`** (object): - - **Description:** Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. - - Note: Currently only the `run_shell_command` tool is supported. - - **Default:** `{}` (Disabled by default) - - **Example:** - ```json - "summarizeToolOutput": { - "run_shell_command": { - "tokenBudget": 2000 - } - } - ``` - -- **`excludedProjectEnvVars`** (array of strings): - - **Description:** 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. - - **Default:** `["DEBUG", "DEBUG_MODE"]` - - **Example:** - ```json - "excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"] - ``` - -- **`includeDirectories`** (array of strings): - - **Description:** 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. - - **Default:** `[]` - - **Example:** - ```json - "includeDirectories": [ - "/path/to/another/project", - "../shared-library", - "~/common-utils" - ] - ``` - -- **`loadMemoryFromIncludeDirectories`** (boolean): - - **Description:** 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. - - **Default:** `false` - - **Example:** - ```json - "loadMemoryFromIncludeDirectories": true - ``` - -- **`tavilyApiKey`** (string): - - **Description:** API key for Tavily web search service. Used to enable the `web_search` tool functionality. - - **Note:** 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. - - **Default:** `undefined` (web search disabled) - - **Example:** `"tavilyApiKey": "tvly-your-api-key-here"` -- **`chatCompression`** (object): - - **Description:** Controls the settings for chat history compression, both automatic and - when manually invoked through the /compress command. - - **Properties:** - - **`contextPercentageThreshold`** (number): A value between 0 and 1 that specifies the token threshold for compression as a percentage of the model's total token limit. For example, a value of `0.6` will trigger compression when the chat history exceeds 60% of the token limit. - - **Example:** - ```json - "chatCompression": { - "contextPercentageThreshold": 0.6 - } - ``` - -- **`showLineNumbers`** (boolean): - - **Description:** Controls whether line numbers are displayed in code blocks in the CLI output. - - **Default:** `true` - - **Example:** - ```json - "showLineNumbers": false - ``` - -- **`accessibility`** (object): - - **Description:** Configures accessibility features for the CLI. - - **Properties:** - - **`screenReader`** (boolean): Enables screen reader mode, which adjusts the TUI for better compatibility with screen readers. This can also be enabled with the `--screen-reader` command-line flag, which will take precedence over the setting. - - **`disableLoadingPhrases`** (boolean): Disables the display of loading phrases during operations. - - **Default:** `{"screenReader": false, "disableLoadingPhrases": false}` - - **Example:** - ```json - "accessibility": { - "screenReader": true, - "disableLoadingPhrases": true - } - ``` - -- **`skipNextSpeakerCheck`** (boolean): - - **Description:** Skips the next speaker check after text responses. When enabled, the system bypasses analyzing whether the AI should continue speaking. - - **Default:** `false` - - **Example:** - ```json - "skipNextSpeakerCheck": true - ``` - -- **`skipLoopDetection`** (boolean): - - **Description:** Disables all loop detection checks (streaming and LLM-based). 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. - - **Default:** `false` - - **Example:** - ```json - "skipLoopDetection": true - ``` - -- **`approvalMode`** (string): - - **Description:** Sets the default approval mode for tool usage. Accepted values are: - - `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. - - **Default:** `"default"` - - **Example:** - ```json - "approvalMode": "plan" - ``` - -### Example `settings.json`: - -```json -{ - "theme": "GitHub", - "sandbox": "docker", - "toolDiscoveryCommand": "bin/get_tools", - "toolCallCommand": "bin/call_tool", - "tavilyApiKey": "$TAVILY_API_KEY", - "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 - }, - "usageStatisticsEnabled": true, - "hideTips": false, - "hideBanner": false, - "skipNextSpeakerCheck": false, - "skipLoopDetection": false, - "maxSessionTurns": 10, - "summarizeToolOutput": { - "run_shell_command": { - "tokenBudget": 100 - } - }, - "excludedProjectEnvVars": ["DEBUG", "DEBUG_MODE", "NODE_ENV"], - "includeDirectories": ["path/to/dir1", "~/path/to/dir2", "../path/to/dir3"], - "loadMemoryFromIncludeDirectories": true -} -``` - -## 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 API keys or for settings that might change between environments. For authentication setup, see the [Authentication documentation](./authentication.md) which covers all available authentication methods. - -The CLI automatically loads environment variables from an `.env` file. The loading order is: - -1. `.env` file in the current working directory. -2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory. -3. If still not found, it looks for `~/.env` (in the user's home directory). - -**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 `excludedProjectEnvVars` setting in your `settings.json` file. - -- **`OPENAI_API_KEY`**: - - One of several available [authentication methods](./authentication.md). - - Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file. -- **`OPENAI_BASE_URL`**: - - One of several available [authentication methods](./authentication.md). - - Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file. -- **`OPENAI_MODEL`**: - - Specifies the default OPENAI model to use. - - Overrides the hardcoded default - - Example: `export OPENAI_MODEL="qwen3-coder-plus"` -- **`GEMINI_SANDBOX`**: - - Alternative to the `sandbox` setting in `settings.json`. - - Accepts `true`, `false`, `docker`, `podman`, or a custom command string. -- **`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. -- **`TAVILY_API_KEY`**: - - Your API key for the Tavily web search service. - - Used to enable the `web_search` tool functionality. - - **Note:** For Qwen OAuth users, DashScope provider is automatically available without any configuration. For other authentication types, configure Tavily or Google providers to enable web search. - - Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` - -## Command-Line Arguments - -Arguments passed directly when running the CLI can override other configurations for that specific session. - -- **`--model `** (**`-m `**): - - Specifies the Qwen model to use for this session. - - 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. -- **`--prompt-interactive `** (**`-i `**): - - Starts an interactive session with the provided prompt as the initial input. - - 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"` -- **`--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`** (or **`-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. 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` -- **`--allowed-tools `**: - - A comma-separated list of tool names that will bypass the confirmation dialog. - - Example: `qwen --allowed-tools "ShellTool(git status)"` -- **`--telemetry`**: - - Enables [telemetry](../telemetry.md). -- **`--telemetry-target`**: - - Sets the telemetry target. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-otlp-endpoint`**: - - Sets the OTLP endpoint for telemetry. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-otlp-protocol`**: - - Sets the OTLP protocol for telemetry (`grpc` or `http`). Defaults to `grpc`. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-log-prompts`**: - - Enables logging of prompts for telemetry. See [telemetry](../telemetry.md) for more information. -- **`--checkpointing`**: - - Enables [checkpointing](../checkpointing.md). -- **`--extensions `** (**`-e `**): - - Specifies a list of extensions to use for the session. 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. - - Example: `--proxy http://localhost:7890`. -- **`--include-directories `**: - - Includes additional directories in the workspace for multi-directory support. - - 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 for accessibility. -- **`--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. 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. - - 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 `contextFileName` 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: - -```markdown -# 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 sophisticated 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. - 3. **Sub-directory Context Files (Contextual/Local):** - - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with a `memoryDiscoveryMaxDirs` field in your `settings.json` file. - - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project. -- **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](../core/memport.md). -- **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](./commands.md#memory) 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. - -## Sandboxing - -Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system. - -Sandboxing is disabled by default, but you can enable it in a few ways: - -- Using `--sandbox` or `-s` flag. -- Setting `GEMINI_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: - -```dockerfile -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: - -```bash -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` in your `settings.json` file: - -```json -{ - "usageStatisticsEnabled": false -} -``` - -Note: When usage statistics are enabled, events are sent to an Alibaba Cloud RUM collection endpoint. - -- **`enableWelcomeBack`** (boolean): - - **Description:** Show welcome back dialog when returning to a project with conversation history. - - **Default:** `true` - - **Category:** UI - - **Requires Restart:** No - - **Example:** `"enableWelcomeBack": false` - - **Details:** 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. This feature integrates with the `/summary` command. See the [Welcome Back documentation](./welcome-back.md) for more details. diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md deleted file mode 100644 index aef6bc4f..00000000 --- a/docs/cli/configuration.md +++ /dev/null @@ -1,757 +0,0 @@ -# Qwen Code Configuration - -**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. - -For details on the previous format, please see the [v1 Configuration documentation](./configuration-v1.md). - -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): - -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: - -- **System defaults file:** - - **Location:** `/etc/qwen-code/system-defaults.json` (Linux), `C:\ProgramData\qwen-code\system-defaults.json` (Windows) or `/Library/Application Support/QwenCode/system-defaults.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_DEFAULTS_PATH` environment variable. - - **Scope:** 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:** - - **Location:** `~/.qwen/settings.json` (where `~` is your home directory). - - **Scope:** Applies to all Qwen Code sessions for the current user. -- **Project settings file:** - - **Location:** `.qwen/settings.json` within your project's root directory. - - **Scope:** Applies only when running Qwen Code from that specific project. Project settings override user settings. - -- **System settings file:** - - **Location:** `/etc/qwen-code/settings.json` (Linux), `C:\ProgramData\qwen-code\settings.json` (Windows) or `/Library/Application Support/QwenCode/settings.json` (macOS). The path can be overridden using the `QWEN_CODE_SYSTEM_SETTINGS_PATH` environment variable. - - **Scope:** 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 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](#sandboxing) (e.g., `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`). - -### 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` - -- **`general.preferredEditor`** (string): - - **Description:** The preferred editor to open files in. - - **Default:** `undefined` - -- **`general.vimMode`** (boolean): - - **Description:** Enable Vim keybindings. - - **Default:** `false` - -- **`general.disableAutoUpdate`** (boolean): - - **Description:** Disable automatic updates. - - **Default:** `false` - -- **`general.disableUpdateNag`** (boolean): - - **Description:** Disable update notification prompts. - - **Default:** `false` - -- **`general.checkpointing.enabled`** (boolean): - - **Description:** Enable session checkpointing for recovery. - - **Default:** `false` - -#### `output` - -- **`output.format`** (string): - - **Description:** The format of the CLI output. - - **Default:** `"text"` - - **Values:** `"text"`, `"json"` - -#### `ui` - -- **`ui.theme`** (string): - - **Description:** The color theme for the UI. See [Themes](./themes.md) for available options. - - **Default:** `undefined` - -- **`ui.customThemes`** (object): - - **Description:** Custom theme definitions. - - **Default:** `{}` - -- **`ui.hideWindowTitle`** (boolean): - - **Description:** Hide the window title bar. - - **Default:** `false` - -- **`ui.hideTips`** (boolean): - - **Description:** Hide helpful tips in the UI. - - **Default:** `false` - -- **`ui.hideBanner`** (boolean): - - **Description:** Hide the application banner. - - **Default:** `false` - -- **`ui.hideFooter`** (boolean): - - **Description:** Hide the footer from the UI. - - **Default:** `false` - -- **`ui.showMemoryUsage`** (boolean): - - **Description:** Display memory usage information in the UI. - - **Default:** `false` - -- **`ui.showLineNumbers`** (boolean): - - **Description:** Show line numbers in the chat. - - **Default:** `false` - -- **`ui.showCitations`** (boolean): - - **Description:** Show citations for generated text in the chat. - - **Default:** `true` - -- **`enableWelcomeBack`** (boolean): - - **Description:** Show welcome back dialog when returning to a project with conversation history. - - **Default:** `true` - -- **`ui.accessibility.disableLoadingPhrases`** (boolean): - - **Description:** Disable loading phrases for accessibility. - - **Default:** `false` - -- **`ui.customWittyPhrases`** (array of strings): - - **Description:** A list of custom phrases to display during loading states. When provided, the CLI will cycle through these phrases instead of the default ones. - - **Default:** `[]` - -#### `ide` - -- **`ide.enabled`** (boolean): - - **Description:** Enable IDE integration mode. - - **Default:** `false` - -- **`ide.hasSeenNudge`** (boolean): - - **Description:** Whether the user has seen the IDE integration nudge. - - **Default:** `false` - -#### `privacy` - -- **`privacy.usageStatisticsEnabled`** (boolean): - - **Description:** Enable collection of usage statistics. - - **Default:** `true` - -#### `model` - -- **`model.name`** (string): - - **Description:** The Qwen model to use for conversations. - - **Default:** `undefined` - -- **`model.maxSessionTurns`** (number): - - **Description:** Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. - - **Default:** `-1` - -- **`model.summarizeToolOutput`** (object): - - **Description:** Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` - - **Default:** `undefined` - -- **`model.chatCompression.contextPercentageThreshold`** (number): - - **Description:** 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. - - **Default:** `0.7` - -- **`model.generationConfig`** (object): - - **Description:** Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, and `disableCacheControl`, along with fine-tuning knobs under `samplingParams` (for example `temperature`, `top_p`, `max_tokens`). Leave unset to rely on provider defaults. - - **Default:** `undefined` - - **Example:** - - ```json - { - "model": { - "generationConfig": { - "timeout": 60000, - "disableCacheControl": false, - "samplingParams": { - "temperature": 0.2, - "top_p": 0.8, - "max_tokens": 1024 - } - } - } - } - ``` - -- **`model.skipNextSpeakerCheck`** (boolean): - - **Description:** Skip the next speaker check. - - **Default:** `false` - -- **`model.skipLoopDetection`**(boolean): - - **Description:** 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. - - **Default:** `false` - -- **`model.skipStartupContext`** (boolean): - - **Description:** 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. - - **Default:** `false` - -- **`model.enableOpenAILogging`** (boolean): - - **Description:** Enables logging of OpenAI API calls for debugging and analysis. When enabled, API requests and responses are logged to JSON files. - - **Default:** `false` - -- **`model.openAILoggingDir`** (string): - - **Description:** 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). - - **Default:** `undefined` - - **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` - -#### `context` - -- **`context.fileName`** (string or array of strings): - - **Description:** The name of the context file(s). - - **Default:** `undefined` - -- **`context.importFormat`** (string): - - **Description:** The format to use when importing memory. - - **Default:** `undefined` - -- **`context.discoveryMaxDirs`** (number): - - **Description:** Maximum number of directories to search for memory. - - **Default:** `200` - -- **`context.includeDirectories`** (array): - - **Description:** Additional directories to include in the workspace context. Missing directories will be skipped with a warning. - - **Default:** `[]` - -- **`context.loadFromIncludeDirectories`** (boolean): - - **Description:** 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. - - **Default:** `false` - -- **`context.fileFiltering.respectGitIgnore`** (boolean): - - **Description:** Respect .gitignore files when searching. - - **Default:** `true` - -- **`context.fileFiltering.respectQwenIgnore`** (boolean): - - **Description:** Respect .qwenignore files when searching. - - **Default:** `true` - -- **`context.fileFiltering.enableRecursiveFileSearch`** (boolean): - - **Description:** Whether to enable searching recursively for filenames under the current tree when completing `@` prefixes in the prompt. - - **Default:** `true` - -#### `tools` - -- **`tools.sandbox`** (boolean or string): - - **Description:** Sandbox execution environment (can be a boolean or a path string). - - **Default:** `undefined` - -- **`tools.shell.enableInteractiveShell`** (boolean): - - Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. Defaults to `false`. - -- **`tools.core`** (array of strings): - - **Description:** This can be used to restrict the set of built-in tools [with an allowlist](./enterprise.md#restricting-tool-access). See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools. The match semantics are the same as `tools.allowed`. - - **Default:** `undefined` - -- **`tools.exclude`** (array of strings): - - **Description:** Tool names to exclude from discovery. - - **Default:** `undefined` - -- **`tools.allowed`** (array of strings): - - **Description:** A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. For example, `["run_shell_command(git)", "run_shell_command(npm test)"]` will skip the confirmation dialog to run any `git` and `npm test` commands. See [Shell Tool command restrictions](../tools/shell.md#command-restrictions) for details on prefix matching, command chaining, etc. - - **Default:** `undefined` - -- **`tools.approvalMode`** (string): - - **Description:** Sets the default approval mode for tool usage. Accepted values are: - - `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. - - **Default:** `default` - -- **`tools.discoveryCommand`** (string): - - **Description:** Command to run for tool discovery. - - **Default:** `undefined` - -- **`tools.callCommand`** (string): - - **Description:** 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). - - **Default:** `undefined` - -- **`tools.useRipgrep`** (boolean): - - **Description:** Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. - - **Default:** `true` - -- **`tools.useBuiltinRipgrep`** (boolean): - - **Description:** Use the bundled ripgrep binary. When set to `false`, the system-level `rg` command will be used instead. This setting is only effective when `tools.useRipgrep` is `true`. - - **Default:** `true` - -- **`tools.enableToolOutputTruncation`** (boolean): - - **Description:** Enable truncation of large tool outputs. - - **Default:** `true` - - **Requires restart:** Yes - -- **`tools.truncateToolOutputThreshold`** (number): - - **Description:** Truncate tool output if it is larger than this many characters. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. - - **Default:** `25000` - - **Requires restart:** Yes - -- **`tools.truncateToolOutputLines`** (number): - - **Description:** Maximum lines or entries kept when truncating tool output. Applies to Shell, Grep, Glob, ReadFile and ReadManyFiles tools. - - **Default:** `1000` - - **Requires restart:** Yes - -#### `mcp` - -- **`mcp.serverCommand`** (string): - - **Description:** Command to start an MCP server. - - **Default:** `undefined` - -- **`mcp.allowed`** (array of strings): - - **Description:** An allowlist of MCP servers to allow. - - **Default:** `undefined` - -- **`mcp.excluded`** (array of strings): - - **Description:** A denylist of MCP servers to exclude. - - **Default:** `undefined` - -#### `security` - -- **`security.folderTrust.enabled`** (boolean): - - **Description:** Setting to track whether Folder trust is enabled. - - **Default:** `false` - -- **`security.auth.selectedType`** (string): - - **Description:** The currently selected authentication type. - - **Default:** `undefined` - -- **`security.auth.enforcedType`** (string): - - **Description:** The required auth type (useful for enterprises). - - **Default:** `undefined` - -- **`security.auth.useExternal`** (boolean): - - **Description:** Whether to use an external authentication flow. - - **Default:** `undefined` - -#### `advanced` - -- **`advanced.autoConfigureMemory`** (boolean): - - **Description:** Automatically configure Node.js memory limits. - - **Default:** `false` - -- **`advanced.dnsResolutionOrder`** (string): - - **Description:** The DNS resolution order. - - **Default:** `undefined` - -- **`advanced.excludedEnvVars`** (array of strings): - - **Description:** Environment variables to exclude from project context. - - **Default:** `["DEBUG","DEBUG_MODE"]` - -- **`advanced.bugCommand`** (object): - - **Description:** Configuration for the bug report command. - - **Default:** `undefined` - -- **`advanced.tavilyApiKey`** (string): - - **Description:** API key for Tavily web search service. Used to enable the `web_search` tool functionality. - - **Note:** 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. - - **Default:** `undefined` - -#### `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`. - -- **`mcpServers.`** (object): The server parameters for the named server. - - `command` (string, optional): The command to execute to start the MCP server via standard I/O. - - `args` (array of strings, optional): Arguments to pass to the command. - - `env` (object, optional): Environment variables to set for the server process. - - `cwd` (string, optional): The working directory in which to start the server. - - `url` (string, optional): The URL of an MCP server that uses Server-Sent Events (SSE) for communication. - - `httpUrl` (string, optional): The URL of an MCP server that uses streamable HTTP for communication. - - `headers` (object, optional): A map of HTTP headers to send with requests to `url` or `httpUrl`. - - `timeout` (number, optional): Timeout in milliseconds for requests to this MCP server. - - `trust` (boolean, optional): Trust this server and bypass all tool call confirmations. - - `description` (string, optional): A brief description of the server, which may be used for display purposes. - - `includeTools` (array of strings, optional): 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. - - `excludeTools` (array of strings, optional): 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. - -#### `telemetry` - -Configures logging and metrics collection for Qwen Code. For more information, see [Telemetry](../telemetry.md). - -- **Properties:** - - **`enabled`** (boolean): Whether or not telemetry is enabled. - - **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`. - - **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter. - - **`otlpProtocol`** (string): The protocol for the OTLP Exporter (`grpc` or `http`). - - **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs. - - **`outfile`** (string): The file to write telemetry to when `target` is `local`. - - **`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: - -```json -{ - "general": { - "vimMode": true, - "preferredEditor": "code" - }, - "ui": { - "theme": "GitHub", - "hideBanner": true, - "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", - "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", - "summarizeToolOutput": { - "run_shell_command": { - "tokenBudget": 100 - } - } - }, - "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 API keys or for settings that might change between environments. For authentication setup, see the [Authentication documentation](./authentication.md) which covers all available authentication methods. - -The CLI automatically loads environment variables from an `.env` file. The loading order is: - -1. `.env` file in the current working directory. -2. If not found, it searches upwards in parent directories until it finds an `.env` file or reaches the project root (identified by a `.git` folder) or the home directory. -3. If still not found, it looks for `~/.env` (in the user's home directory). - -**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. - -- **`OPENAI_API_KEY`**: - - One of several available [authentication methods](./authentication.md). - - Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file. -- **`OPENAI_BASE_URL`**: - - One of several available [authentication methods](./authentication.md). - - Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file. -- **`OPENAI_MODEL`**: - - Specifies the default OPENAI model to use. - - Overrides the hardcoded default - - Example: `export OPENAI_MODEL="qwen3-coder-plus"` -- **`GEMINI_TELEMETRY_ENABLED`**: - - Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. - - Overrides the `telemetry.enabled` setting. -- **`GEMINI_TELEMETRY_TARGET`**: - - Sets the telemetry target (`local` or `gcp`). - - Overrides the `telemetry.target` setting. -- **`GEMINI_TELEMETRY_OTLP_ENDPOINT`**: - - Sets the OTLP endpoint for telemetry. - - Overrides the `telemetry.otlpEndpoint` setting. -- **`GEMINI_TELEMETRY_OTLP_PROTOCOL`**: - - Sets the OTLP protocol (`grpc` or `http`). - - Overrides the `telemetry.otlpProtocol` setting. -- **`GEMINI_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. -- **`GEMINI_TELEMETRY_OUTFILE`**: - - Sets the file path to write telemetry to when the target is `local`. - - Overrides the `telemetry.outfile` setting. -- **`GEMINI_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. -- **`GEMINI_SANDBOX`**: - - Alternative to the `sandbox` setting in `settings.json`. - - Accepts `true`, `false`, `docker`, `podman`, or a custom command string. -- **`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. -- **`TAVILY_API_KEY`**: - - Your API key for the Tavily web search service. - - Used to enable the `web_search` tool functionality. - - **Note:** For Qwen OAuth users, DashScope provider is automatically available without any configuration. For other authentication types, configure Tavily or Google providers to enable web search. - - Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` - -## Command-Line Arguments - -Arguments passed directly when running the CLI can override other configurations for that specific session. - -- **`--model `** (**`-m `**): - - Specifies the Qwen model to use for this session. - - 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. - - 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. - - 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"` -- **`--continue`**: - - Resume the most recent session for the current project (current working directory). - - Works in interactive and headless modes (e.g., `qwen --continue -p "Keep going"`). -- **`--resume [sessionId]`**: - - Resume a specific session for the current project. When called without an ID, an interactive picker lists only this project's sessions with prompt preview, timestamps, message count, and optional git branch. - - If an ID is provided and not found for this project, the CLI exits with an error. -- **`--output-format `** (**`-o `**): - - **Description:** Specifies the format of the CLI output for non-interactive mode. - - **Values:** - - `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. - - **Note:** For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless.md) for detailed information. -- **`--input-format `**: - - **Description:** Specifies the format consumed from standard input. - - **Values:** - - `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. - - **Note:** When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless.md) for detailed information. -- **`--include-partial-messages`**: - - **Description:** 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. - - **Note:** See [Headless Mode](../features/headless.md) 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`** (or **`-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. 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` -- **`--allowed-tools `**: - - A comma-separated list of tool names that will bypass the confirmation dialog. - - Example: `qwen --allowed-tools "Shell(git status)"` -- **`--telemetry`**: - - Enables [telemetry](../telemetry.md). -- **`--telemetry-target`**: - - Sets the telemetry target. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-otlp-endpoint`**: - - Sets the OTLP endpoint for telemetry. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-otlp-protocol`**: - - Sets the OTLP protocol for telemetry (`grpc` or `http`). Defaults to `grpc`. See [telemetry](../telemetry.md) for more information. -- **`--telemetry-log-prompts`**: - - Enables logging of prompts for telemetry. See [telemetry](../telemetry.md) for more information. -- **`--checkpointing`**: - - Enables [checkpointing](../checkpointing.md). -- **`--extensions `** (**`-e `**): - - Specifies a list of extensions to use for the session. 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. - - Example: `--proxy http://localhost:7890`. -- **`--include-directories `**: - - Includes additional directories in the workspace for multi-directory support. - - 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. 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. - - 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: - -```markdown -# 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 sophisticated 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. - 3. **Sub-directory Context Files (Contextual/Local):** - - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file. - - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project. -- **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](../core/memport.md). -- **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](./commands.md#memory) 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. - -## Sandboxing - -Qwen Code can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system. - -Sandboxing is disabled by default, but you can enable it in a few ways: - -- Using `--sandbox` or `-s` flag. -- Setting `GEMINI_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: - -```dockerfile -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: - -```bash -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: - -```json -{ - "privacy": { - "usageStatisticsEnabled": false - } -} -``` - -Note: When usage statistics are enabled, events are sent to an Alibaba Cloud RUM collection endpoint. diff --git a/docs/cli/index.md b/docs/cli/index.md deleted file mode 100644 index e5d3ddc6..00000000 --- a/docs/cli/index.md +++ /dev/null @@ -1,29 +0,0 @@ -# Qwen Code CLI - -Within Qwen Code, `packages/cli` is the frontend for users to send and receive prompts with Qwen and other AI models and their associated tools. For a general overview of Qwen Code - -## Navigating this section - -- **[Authentication](./authentication.md):** A guide to setting up authentication with Qwen OAuth and OpenAI-compatible providers. -- **[Commands](./commands.md):** A reference for Qwen Code CLI commands (e.g., `/help`, `/tools`, `/theme`). -- **[Configuration](./configuration.md):** A guide to tailoring Qwen Code CLI behavior using configuration files. -- **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes. -- **[Tutorials](tutorials.md)**: A tutorial showing how to use Qwen Code to automate a development task. - -## Non-interactive mode - -Qwen Code can be run in a non-interactive mode, which is useful for scripting and automation. In this mode, you pipe input to the CLI, it executes the command, and then it exits. - -The following example pipes a command to Qwen Code from your terminal: - -```bash -echo "What is fine tuning?" | qwen -``` - -You can also use the `--prompt` or `-p` flag: - -```bash -qwen -p "What is fine tuning?" -``` - -For comprehensive documentation on headless usage, scripting, automation, and advanced examples, see the **[Headless Mode](../headless.md)** guide. diff --git a/docs/cli/language.md b/docs/cli/language.md deleted file mode 100644 index 7fb1e7f0..00000000 --- a/docs/cli/language.md +++ /dev/null @@ -1,71 +0,0 @@ -# Language Command - -The `/language` command allows you to customize the language settings for both the Qwen Code user interface (UI) and the language model's output. This command supports two distinct functionalities: - -1. Setting the UI language for the Qwen Code interface -2. Setting the output language for the language model (LLM) - -## UI Language Settings - -To change the UI language of Qwen Code, use the `ui` subcommand: - -``` -/language ui [zh-CN|en-US] -``` - -### Available UI Languages - -- **zh-CN**: Simplified Chinese (简体中文) -- **en-US**: English - -### Examples - -``` -/language ui zh-CN # Set UI language to Simplified Chinese -/language ui en-US # Set UI language to English -``` - -### UI Language Subcommands - -You can also use direct subcommands for convenience: - -- `/language ui zh-CN` or `/language ui zh` or `/language ui 中文` -- `/language ui en-US` or `/language ui en` or `/language ui english` - -## LLM Output Language Settings - -To set the language for the language model's responses, use the `output` subcommand: - -``` -/language output -``` - -This command generates a language rule file that instructs the LLM to respond in the specified language. The rule file is saved to `~/.qwen/output-language.md`. - -### Examples - -``` -/language output 中文 # Set LLM output language to Chinese -/language output English # Set LLM output language to English -/language output 日本語 # Set LLM output language to Japanese -``` - -## Viewing Current Settings - -When used without arguments, the `/language` command displays the current language settings: - -``` -/language -``` - -This will show: - -- Current UI language -- Current LLM output language (if set) -- Available subcommands - -## Notes - -- UI language changes take effect immediately and reload all command descriptions -- LLM output language settings are persisted in a rule file that is automatically included in the model's context -- To request additional UI language packs, please open an issue on GitHub diff --git a/docs/cli/openai-auth.md b/docs/cli/openai-auth.md deleted file mode 100644 index 9dd8c0ca..00000000 --- a/docs/cli/openai-auth.md +++ /dev/null @@ -1,76 +0,0 @@ -# OpenAI Authentication - -Qwen Code CLI supports OpenAI authentication for users who want to use OpenAI models instead of Google's Gemini models. - -## Authentication Methods - -### 1. Interactive Authentication (Recommended) - -When you first run the CLI and select OpenAI as your authentication method, you'll be prompted to enter: - -- **API Key**: Your OpenAI API key from [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys) -- **Base URL**: The base URL for OpenAI API (defaults to `https://api.openai.com/v1`) -- **Model**: The OpenAI model to use (defaults to `gpt-4o`) - -The CLI will guide you through each field: - -1. Enter your API key and press Enter -2. Review/modify the base URL and press Enter -3. Review/modify the model name and press Enter - -**Note**: You can paste your API key directly - the CLI supports paste functionality and will display the full key for verification. - -### 2. Command Line Arguments - -You can also provide the OpenAI credentials via command line arguments: - -```bash -# Basic usage with API key -qwen-code --openai-api-key "your-api-key-here" - -# With custom base URL -qwen-code --openai-api-key "your-api-key-here" --openai-base-url "https://your-custom-endpoint.com/v1" - -# With custom model -qwen-code --openai-api-key "your-api-key-here" --model "gpt-4-turbo" -``` - -### 3. Environment Variables - -Set the following environment variables in your shell or `.env` file: - -```bash -export OPENAI_API_KEY="your-api-key-here" -export OPENAI_BASE_URL="https://api.openai.com/v1" # Optional, defaults to this value -export OPENAI_MODEL="gpt-4o" # Optional, defaults to gpt-4o -``` - -## Supported Models - -The CLI supports all OpenAI models that are available through the OpenAI API, including: - -- `gpt-4o` (default) -- `gpt-4o-mini` -- `gpt-4-turbo` -- `gpt-4` -- `gpt-3.5-turbo` -- And other available models - -## Custom Endpoints - -You can use custom endpoints by setting the `OPENAI_BASE_URL` environment variable or using the `--openai-base-url` command line argument. This is useful for: - -- Using Azure OpenAI -- Using other OpenAI-compatible APIs -- Using local OpenAI-compatible servers - -## Switching Authentication Methods - -To switch between authentication methods, use the `/auth` command in the CLI interface. - -## Security Notes - -- API keys are stored in memory during the session -- For persistent storage, use environment variables or `.env` files -- Never commit API keys to version control -- The CLI displays API keys in plain text for verification - ensure your terminal is secure diff --git a/docs/cli/tutorials.md b/docs/cli/tutorials.md deleted file mode 100644 index bc43d6d6..00000000 --- a/docs/cli/tutorials.md +++ /dev/null @@ -1,69 +0,0 @@ -# Tutorials - -This page contains tutorials for interacting with Qwen Code. - -## Setting up a Model Context Protocol (MCP) server - -> [!CAUTION] -> Before using a third-party MCP server, ensure you trust its source and understand the tools it provides. Your use of third-party servers is at your own risk. - -This tutorial demonstrates how to set up a MCP server, using the [GitHub MCP server](https://github.com/github/github-mcp-server) as an example. The GitHub MCP server provides tools for interacting with GitHub repositories, such as creating issues and commenting on pull requests. - -### Prerequisites - -Before you begin, ensure you have the following installed and configured: - -- **Docker:** Install and run [Docker]. -- **GitHub Personal Access Token (PAT):** Create a new [classic] or [fine-grained] PAT with the necessary scopes. - -[Docker]: https://www.docker.com/ -[classic]: https://github.com/settings/tokens/new -[fine-grained]: https://github.com/settings/personal-access-tokens/new - -### Guide - -#### Configure the MCP server in `settings.json` - -In your project's root directory, create or open the [`.qwen/settings.json` file](./configuration.md). Within the file, add the `mcpServers` configuration block, which provides instructions for how to launch the GitHub MCP server. - -```json -{ - "mcpServers": { - "github": { - "command": "docker", - "args": [ - "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server" - ], - "env": { - "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" - } - } - } -} -``` - -#### Set your GitHub token - -> [!CAUTION] -> Using a broadly scoped personal access token that has access to personal and private repositories can lead to information from the private repository being leaked into the public repository. We recommend using a fine-grained access token that doesn't share access to both public and private repositories. - -Use an environment variable to store your GitHub PAT: - -```bash -GITHUB_PERSONAL_ACCESS_TOKEN="pat_YourActualGitHubTokenHere" -``` - -Qwen Code uses this value in the `mcpServers` configuration that you defined in the `settings.json` file. - -#### Launch Qwen Code and verify the connection - -When you launch Qwen Code, it automatically reads your configuration and launches the GitHub MCP server in the background. You can then use natural language prompts to ask Qwen Code to perform GitHub actions. For example: - -```bash -"get all open issues assigned to me in the 'foo/bar' repo and prioritize them" -``` diff --git a/docs/core/index.md b/docs/core/index.md deleted file mode 100644 index 8ac4d4d1..00000000 --- a/docs/core/index.md +++ /dev/null @@ -1,55 +0,0 @@ -# Qwen Code Core - -Qwen Code's core package (`packages/core`) is the backend portion of Qwen Code, handling communication with model APIs, managing tools, and processing requests sent from `packages/cli`. For a general overview of Qwen Code, see the [main documentation page](../index.md). - -## Navigating this section - -- **[Core tools API](./tools-api.md):** Information on how tools are defined, registered, and used by the core. -- **[Memory Import Processor](./memport.md):** Documentation for the modular QWEN.md import feature using @file.md syntax. - -## Role of the core - -While the `packages/cli` portion of Qwen Code provides the user interface, `packages/core` is responsible for: - -- **Model API interaction:** Securely communicating with the configured model provider, sending user prompts, and receiving model responses. -- **Prompt engineering:** Constructing effective prompts for the model, potentially incorporating conversation history, tool definitions, and instructional context from context files (e.g., `QWEN.md`). -- **Tool management & orchestration:** - - Registering available tools (e.g., file system tools, shell command execution). - - Interpreting tool use requests from the model. - - Executing the requested tools with the provided arguments. - - Returning tool execution results to the model for further processing. -- **Session and state management:** Keeping track of the conversation state, including history and any relevant context required for coherent interactions. -- **Configuration:** Managing core-specific configurations, such as API key access, model selection, and tool settings. - -## Security considerations - -The core plays a vital role in security: - -- **API key management:** It handles provider credentials and ensures they're used securely when communicating with APIs. -- **Tool execution:** When tools interact with the local system (e.g., `run_shell_command`), the core (and its underlying tool implementations) must do so with appropriate caution, often involving sandboxing mechanisms to prevent unintended modifications. - -## Chat history compression - -To ensure that long conversations don't exceed the token limits of the selected model, the core includes a chat history compression feature. - -When a conversation approaches the token limit for the configured model, the core automatically compresses the conversation history before sending it to the model. This compression is designed to be lossless in terms of the information conveyed, but it reduces the overall number of tokens used. - -You can find token limits for each provider's models in their documentation. - -## Model fallback - -Qwen Code includes a model fallback mechanism to ensure that you can continue to use the CLI even if the default model is rate-limited. - -If you are using the default "pro" model and the CLI detects that you are being rate-limited, it automatically switches to the "flash" model for the current session. This allows you to continue working without interruption. - -## File discovery service - -The file discovery service is responsible for finding files in the project that are relevant to the current context. It is used by the `@` command and other tools that need to access files. - -## Memory discovery service - -The memory discovery service is responsible for finding and loading the context files (default: `QWEN.md`) that provide context to the model. It searches for these files in a hierarchical manner, starting from the current working directory and moving up to the project root and the user's home directory. It also searches in subdirectories. - -This allows you to have global, project-level, and component-level context files, which are all combined to provide the model with the most relevant information. - -You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and `refresh` the content of loaded context files. diff --git a/docs/core/memport.md b/docs/core/memport.md deleted file mode 100644 index 3431653a..00000000 --- a/docs/core/memport.md +++ /dev/null @@ -1,215 +0,0 @@ -# Memory Import Processor - -The Memory Import Processor is a feature that allows you to modularize your context files (e.g., `QWEN.md`) by importing content from other files using the `@file.md` syntax. - -## Overview - -This feature enables you to break down large context files (e.g., `QWEN.md`) into smaller, more manageable components that can be reused across different contexts. The import processor supports both relative and absolute paths, with built-in safety features to prevent circular imports and ensure file access security. - -## Syntax - -Use the `@` symbol followed by the path to the file you want to import: - -```markdown -# Main QWEN.md file - -This is the main content. - -@./components/instructions.md - -More content here. - -@./shared/configuration.md -``` - -## Supported Path Formats - -### Relative Paths - -- `@./file.md` - Import from the same directory -- `@../file.md` - Import from parent directory -- `@./components/file.md` - Import from subdirectory - -### Absolute Paths - -- `@/absolute/path/to/file.md` - Import using absolute path - -## Examples - -### Basic Import - -```markdown -# My QWEN.md - -Welcome to my project! - -@./getting-started.md - -## Features - -@./features/overview.md -``` - -### Nested Imports - -The imported files can themselves contain imports, creating a nested structure: - -```markdown -# main.md - -@./header.md -@./content.md -@./footer.md -``` - -```markdown -# header.md - -# Project Header - -@./shared/title.md -``` - -## Safety Features - -### Circular Import Detection - -The processor automatically detects and prevents circular imports: - -```markdown -# file-a.md - -@./file-b.md - -# file-b.md - -@./file-a.md -``` - -### File Access Security - -The `validateImportPath` function ensures that imports are only allowed from specified directories, preventing access to sensitive files outside the allowed scope. - -### Maximum Import Depth - -To prevent infinite recursion, there's a configurable maximum import depth (default: 5 levels). - -## Error Handling - -### Missing Files - -If a referenced file doesn't exist, the import will fail gracefully with an error comment in the output. - -### File Access Errors - -Permission issues or other file system errors are handled gracefully with appropriate error messages. - -## Code Region Detection - -The import processor uses the `marked` library to detect code blocks and inline code spans, ensuring that `@` imports inside these regions are properly ignored. This provides robust handling of nested code blocks and complex Markdown structures. - -## Import Tree Structure - -The processor returns an import tree that shows the hierarchy of imported files. This helps users debug problems with their context files by showing which files were read and their import relationships. - -Example tree structure: - -``` - Memory Files - L project: QWEN.md - L a.md - L b.md - L c.md - L d.md - L e.md - L f.md - L included.md -``` - -The tree preserves the order that files were imported and shows the complete import chain for debugging purposes. - -## Comparison to Claude Code's `/memory` (`claude.md`) Approach - -Claude Code's `/memory` feature (as seen in `claude.md`) produces a flat, linear document by concatenating all included files, always marking file boundaries with clear comments and path names. It does not explicitly present the import hierarchy, but the LLM receives all file contents and paths, which is sufficient for reconstructing the hierarchy if needed. - -Note: The import tree is mainly for clarity during development and has limited relevance to LLM consumption. - -## API Reference - -### `processImports(content, basePath, debugMode?, importState?)` - -Processes import statements in context file content. - -**Parameters:** - -- `content` (string): The content to process for imports -- `basePath` (string): The directory path where the current file is located -- `debugMode` (boolean, optional): Whether to enable debug logging (default: false) -- `importState` (ImportState, optional): State tracking for circular import prevention - -**Returns:** Promise<ProcessImportsResult> - Object containing processed content and import tree - -### `ProcessImportsResult` - -```typescript -interface ProcessImportsResult { - content: string; // The processed content with imports resolved - importTree: MemoryFile; // Tree structure showing the import hierarchy -} -``` - -### `MemoryFile` - -```typescript -interface MemoryFile { - path: string; // The file path - imports?: MemoryFile[]; // Direct imports, in the order they were imported -} -``` - -### `validateImportPath(importPath, basePath, allowedDirectories)` - -Validates import paths to ensure they are safe and within allowed directories. - -**Parameters:** - -- `importPath` (string): The import path to validate -- `basePath` (string): The base directory for resolving relative paths -- `allowedDirectories` (string[]): Array of allowed directory paths - -**Returns:** boolean - Whether the import path is valid - -### `findProjectRoot(startDir)` - -Finds the project root by searching for a `.git` directory upwards from the given start directory. Implemented as an **async** function using non-blocking file system APIs to avoid blocking the Node.js event loop. - -**Parameters:** - -- `startDir` (string): The directory to start searching from - -**Returns:** Promise<string> - The project root directory (or the start directory if no `.git` is found) - -## Best Practices - -1. **Use descriptive file names** for imported components -2. **Keep imports shallow** - avoid deeply nested import chains -3. **Document your structure** - maintain a clear hierarchy of imported files -4. **Test your imports** - ensure all referenced files exist and are accessible -5. **Use relative paths** when possible for better portability - -## Troubleshooting - -### Common Issues - -1. **Import not working**: Check that the file exists and the path is correct -2. **Circular import warnings**: Review your import structure for circular references -3. **Permission errors**: Ensure the files are readable and within allowed directories -4. **Path resolution issues**: Use absolute paths if relative paths aren't resolving correctly - -### Debug Mode - -Enable debug mode to see detailed logging of the import process: - -```typescript -const result = await processImports(content, basePath, true); -``` diff --git a/docs/core/tools-api.md b/docs/core/tools-api.md deleted file mode 100644 index 70266b88..00000000 --- a/docs/core/tools-api.md +++ /dev/null @@ -1,79 +0,0 @@ -# Qwen Code Core: Tools API - -The Qwen Code core (`packages/core`) features a robust system for defining, registering, and executing tools. These tools extend the capabilities of the model, allowing it to interact with the local environment, fetch web content, and perform various actions beyond simple text generation. - -## Core Concepts - -- **Tool (`tools.ts`):** An interface and base class (`BaseTool`) that defines the contract for all tools. Each tool must have: - - `name`: A unique internal name (used in API calls to the model). - - `displayName`: A user-friendly name. - - `description`: A clear explanation of what the tool does, which is provided to the model. - - `parameterSchema`: A JSON schema defining the parameters that the tool accepts. This is crucial for the model to understand how to call the tool correctly. - - `validateToolParams()`: A method to validate incoming parameters. - - `getDescription()`: A method to provide a human-readable description of what the tool will do with specific parameters before execution. - - `shouldConfirmExecute()`: A method to determine if user confirmation is required before execution (e.g., for potentially destructive operations). - - `execute()`: The core method that performs the tool's action and returns a `ToolResult`. - -- **`ToolResult` (`tools.ts`):** An interface defining the structure of a tool's execution outcome: - - `llmContent`: The factual content to be included in the history sent back to the LLM for context. This can be a simple string or a `PartListUnion` (an array of `Part` objects and strings) for rich content. - - `returnDisplay`: A user-friendly string (often Markdown) or a special object (like `FileDiff`) for display in the CLI. - -- **Returning Rich Content:** Tools are not limited to returning simple text. The `llmContent` can be a `PartListUnion`, which is an array that can contain a mix of `Part` objects (for images, audio, etc.) and `string`s. This allows a single tool execution to return multiple pieces of rich content. - -- **Tool Registry (`tool-registry.ts`):** A class (`ToolRegistry`) responsible for: - - **Registering Tools:** Holding a collection of all available built-in tools (e.g., `ListFiles`, `ReadFile`). - - **Discovering Tools:** It can also discover tools dynamically: - - **Command-based Discovery:** If `tools.toolDiscoveryCommand` is configured in settings, this command is executed. It's expected to output JSON describing custom tools, which are then registered as `DiscoveredTool` instances. - - **MCP-based Discovery:** If `mcp.mcpServerCommand` is configured, the registry can connect to a Model Context Protocol (MCP) server to list and register tools (`DiscoveredMCPTool`). - - **Providing Schemas:** Exposing the `FunctionDeclaration` schemas of all registered tools to the model, so it knows what tools are available and how to use them. - - **Retrieving Tools:** Allowing the core to get a specific tool by name for execution. - -## Built-in Tools - -The core comes with a suite of pre-defined tools, typically found in `packages/core/src/tools/`. These include: - -- **File System Tools:** - - `ListFiles` (`ls.ts`): Lists directory contents. - - `ReadFile` (`read-file.ts`): Reads the content of a single file. It takes an `absolute_path` parameter, which must be an absolute path. - - `WriteFile` (`write-file.ts`): Writes content to a file. - - `ReadManyFiles` (`read-many-files.ts`): Reads and concatenates content from multiple files or glob patterns (used by the `@` command in CLI). - - `Grep` (`grep.ts`): Searches for patterns in files. - - `Glob` (`glob.ts`): Finds files matching glob patterns. - - `Edit` (`edit.ts`): Performs in-place modifications to files (often requiring confirmation). -- **Execution Tools:** - - `Shell` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation). -- **Web Tools:** - - `WebFetch` (`web-fetch.ts`): Fetches content from a URL. - - `WebSearch` (`web-search.ts`): Performs a web search. -- **Memory Tools:** - - `SaveMemory` (`memoryTool.ts`): Interacts with the AI's memory. -- **Planning Tools:** - - `Task` (`task.ts`): Delegates tasks to specialized subagents. - - `TodoWrite` (`todoWrite.ts`): Creates and manages a structured task list. - - `ExitPlanMode` (`exitPlanMode.ts`): Exits plan mode and returns to normal operation. - -Each of these tools extends `BaseTool` and implements the required methods for its specific functionality. - -## Tool Execution Flow - -1. **Model Request:** The model, based on the user's prompt and the provided tool schemas, decides to use a tool and returns a `FunctionCall` part in its response, specifying the tool name and arguments. -2. **Core Receives Request:** The core parses this `FunctionCall`. -3. **Tool Retrieval:** It looks up the requested tool in the `ToolRegistry`. -4. **Parameter Validation:** The tool's `validateToolParams()` method is called. -5. **Confirmation (if needed):** - - The tool's `shouldConfirmExecute()` method is called. - - If it returns details for confirmation, the core communicates this back to the CLI, which prompts the user. - - The user's decision (e.g., proceed, cancel) is sent back to the core. -6. **Execution:** If validated and confirmed (or if no confirmation is needed), the core calls the tool's `execute()` method with the provided arguments and an `AbortSignal` (for potential cancellation). -7. **Result Processing:** The `ToolResult` from `execute()` is received by the core. -8. **Response to Model:** The `llmContent` from the `ToolResult` is packaged as a `FunctionResponse` and sent back to the model so it can continue generating a user-facing response. -9. **Display to User:** The `returnDisplay` from the `ToolResult` is sent to the CLI to show the user what the tool did. - -## Extending with Custom Tools - -While direct programmatic registration of new tools by users isn't explicitly detailed as a primary workflow in the provided files for typical end-users, the architecture supports extension through: - -- **Command-based Discovery:** Advanced users or project administrators can define a `tools.toolDiscoveryCommand` in `settings.json`. This command, when run by the core, should output a JSON array of `FunctionDeclaration` objects. The core will then make these available as `DiscoveredTool` instances. The corresponding `tools.toolCallCommand` would then be responsible for actually executing these custom tools. -- **MCP Server(s):** For more complex scenarios, one or more MCP servers can be set up and configured via the `mcpServers` setting in `settings.json`. The core can then discover and use tools exposed by these servers. As mentioned, if you have multiple MCP servers, the tool names will be prefixed with the server name from your configuration (e.g., `serverAlias__actualToolName`). - -This tool system provides a flexible and powerful way to augment the model's capabilities, making Qwen Code a versatile assistant for a wide range of tasks. diff --git a/docs/developers/_meta.ts b/docs/developers/_meta.ts new file mode 100644 index 00000000..956e1ad9 --- /dev/null +++ b/docs/developers/_meta.ts @@ -0,0 +1,27 @@ +export default { + 'Contribute to Qwen Code': { + title: 'Contribute to Qwen Code', + type: 'separator', + }, + architecture: 'Architecture', + roadmap: 'Roadmap', + contributing: 'Contributing Guide', + 'Qwen Code SDK': { + title: 'Agent SDK', + type: 'separator', + }, + 'sdk-typescript': 'Typescript SDK', + 'Dive Into Qwen Code': { + title: 'Dive Into Qwen Code', + type: 'separator', + }, + + tools: 'Tools', + + extensions: { + display: 'hidden', + }, + examples: { + display: 'hidden', + }, +}; diff --git a/docs/developers/architecture.md b/docs/developers/architecture.md new file mode 100644 index 00000000..8d9540c0 --- /dev/null +++ b/docs/developers/architecture.md @@ -0,0 +1,95 @@ +# Qwen Code Architecture Overview + +This document provides a high-level overview of Qwen Code's architecture. + +## Core Components + +Qwen Code is primarily composed of two main packages, along with a suite of tools that can be used by the system in the course of handling command-line input: + +### 1. CLI Package (`packages/cli`) + +**Purpose:** This contains the user-facing portion of Qwen Code, such as handling the initial user input, presenting the final output, and managing the overall user experience. + +**Key Functions:** + +- **Input Processing:** Handles user input through various methods including direct text entry, slash commands (e.g., `/help`, `/clear`, `/model`), at commands (`@file` for including file content), and exclamation mark commands (`!command` for shell execution). +- **History Management:** Maintains conversation history and enables features like session resumption. +- **Display Rendering:** Formats and presents responses to the user in the terminal with syntax highlighting and proper formatting. +- **Theme and UI Customization:** Supports customizable themes and UI elements for a personalized experience. +- **Configuration Settings:** Manages various configuration options through JSON settings files, environment variables, and command-line arguments. + +### 2. Core Package (`packages/core`) + +**Purpose:** This acts as the backend for Qwen Code. It receives requests sent from `packages/cli`, orchestrates interactions with the configured model API, and manages the execution of available tools. + +**Key Functions:** + +- **API Client:** Communicates with the Qwen model API to send prompts and receive responses. +- **Prompt Construction:** Builds appropriate prompts for the model, incorporating conversation history and available tool definitions. +- **Tool Registration and Execution:** Manages the registration of available tools and executes them based on model requests. +- **State Management:** Maintains conversation and session state information. +- **Server-side Configuration:** Handles server-side configuration and settings. + +### 3. Tools (`packages/core/src/tools/`) + +**Purpose:** These are individual modules that extend the capabilities of the Qwen model, allowing it to interact with the local environment (e.g., file system, shell commands, web fetching). + +**Interaction:** `packages/core` invokes these tools based on requests from the Qwen model. + +**Common Tools Include:** + +- **File Operations:** Reading, writing, and editing files +- **Shell Commands:** Executing system commands with user approval for potentially dangerous operations +- **Search Tools:** Finding files and searching content within the project +- **Web Tools:** Fetching content from the web +- **MCP Integration:** Connecting to Model Context Protocol servers for extended capabilities + +## Interaction Flow + +A typical interaction with Qwen Code follows this flow: + +1. **User Input:** The user types a prompt or command into the terminal, which is managed by `packages/cli`. +2. **Request to Core:** `packages/cli` sends the user's input to `packages/core`. +3. **Request Processing:** The core package: + - Constructs an appropriate prompt for the configured model API, possibly including conversation history and available tool definitions. + - Sends the prompt to the model API. +4. **Model API Response:** The model API processes the prompt and returns a response. This response might be a direct answer or a request to use one of the available tools. +5. **Tool Execution (if applicable):** + - When the model API requests a tool, the core package prepares to execute it. + - If the requested tool can modify the file system or execute shell commands, the user is first given details of the tool and its arguments, and the user must approve the execution. + - Read-only operations, such as reading files, might not require explicit user confirmation to proceed. + - Once confirmed, or if confirmation is not required, the core package executes the relevant action within the relevant tool, and the result is sent back to the model API by the core package. + - The model API processes the tool result and generates a final response. +6. **Response to CLI:** The core package sends the final response back to the CLI package. +7. **Display to User:** The CLI package formats and displays the response to the user in the terminal. + +## Configuration Options + +Qwen Code offers multiple ways to configure its behavior: + +### Configuration Layers (in order of precedence) + +1. Command-line arguments +2. Environment variables +3. Project settings file (`.qwen/settings.json`) +4. User settings file (`~/.qwen/settings.json`) +5. System settings files +6. Default values + +### Key Configuration Categories + +- **General Settings:** vim mode, preferred editor, auto-update preferences +- **UI Settings:** Theme customization, banner visibility, footer display +- **Model Settings:** Model selection, session turn limits, compression settings +- **Context Settings:** Context file names, directory inclusion, file filtering +- **Tool Settings:** Approval modes, sandboxing, tool restrictions +- **Privacy Settings:** Usage statistics collection +- **Advanced Settings:** Debug options, custom bug reporting commands + +## Key Design Principles + +- **Modularity:** Separating the CLI (frontend) from the Core (backend) allows for independent development and potential future extensions (e.g., different frontends for the same backend). +- **Extensibility:** The tool system is designed to be extensible, allowing new capabilities to be added through custom tools or MCP server integration. +- **User Experience:** The CLI focuses on providing a rich and interactive terminal experience with features like syntax highlighting, customizable themes, and intuitive command structures. +- **Security:** Implements approval mechanisms for potentially dangerous operations and sandboxing options to protect the user's system. +- **Flexibility:** Supports multiple configuration methods and can adapt to different workflows and environments. diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md new file mode 100644 index 00000000..84aa5520 --- /dev/null +++ b/docs/developers/contributing.md @@ -0,0 +1,303 @@ +# How to Contribute + +We would love to accept your patches and contributions to this project. + +## Contribution Process + +### Code Reviews + +All submissions, including submissions by project members, require review. We +use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests) +for this purpose. + +### Pull Request Guidelines + +To help us review and merge your PRs quickly, please follow these guidelines. PRs that do not meet these standards may be closed. + +#### 1. Link to an Existing Issue + +All PRs should be linked to an existing issue in our tracker. This ensures that every change has been discussed and is aligned with the project's goals before any code is written. + +- **For bug fixes:** The PR should be linked to the bug report issue. +- **For features:** The PR should be linked to the feature request or proposal issue that has been approved by a maintainer. + +If an issue for your change doesn't exist, please **open one first** and wait for feedback before you start coding. + +#### 2. Keep It Small and Focused + +We favor small, atomic PRs that address a single issue or add a single, self-contained feature. + +- **Do:** Create a PR that fixes one specific bug or adds one specific feature. +- **Don't:** Bundle multiple unrelated changes (e.g., a bug fix, a new feature, and a refactor) into a single PR. + +Large changes should be broken down into a series of smaller, logical PRs that can be reviewed and merged independently. + +#### 3. Use Draft PRs for Work in Progress + +If you'd like to get early feedback on your work, please use GitHub's **Draft Pull Request** feature. This signals to the maintainers that the PR is not yet ready for a formal review but is open for discussion and initial feedback. + +#### 4. Ensure All Checks Pass + +Before submitting your PR, ensure that all automated checks are passing by running `npm run preflight`. This command runs all tests, linting, and other style checks. + +#### 5. Update Documentation + +If your PR introduces a user-facing change (e.g., a new command, a modified flag, or a change in behavior), you must also update the relevant documentation in the `/docs` directory. + +#### 6. Write Clear Commit Messages and a Good PR Description + +Your PR should have a clear, descriptive title and a detailed description of the changes. Follow the [Conventional Commits](https://www.conventionalcommits.org/) standard for your commit messages. + +- **Good PR Title:** `feat(cli): Add --json flag to 'config get' command` +- **Bad PR Title:** `Made some changes` + +In the PR description, explain the "why" behind your changes and link to the relevant issue (e.g., `Fixes #123`). + +## Development Setup and Workflow + +This section guides contributors on how to build, modify, and understand the development setup of this project. + +### Setting Up the Development Environment + +**Prerequisites:** + +1. **Node.js**: + - **Development:** Please use Node.js `~20.19.0`. This specific version is required due to an upstream development dependency issue. You can use a tool like [nvm](https://github.com/nvm-sh/nvm) to manage Node.js versions. + - **Production:** For running the CLI in a production environment, any version of Node.js `>=20` is acceptable. +2. **Git** + +### Build Process + +To clone the repository: + +```bash +git clone https://github.com/QwenLM/qwen-code.git # Or your fork's URL +cd qwen-code +``` + +To install dependencies defined in `package.json` as well as root dependencies: + +```bash +npm install +``` + +To build the entire project (all packages): + +```bash +npm run build +``` + +This command typically compiles TypeScript to JavaScript, bundles assets, and prepares the packages for execution. Refer to `scripts/build.js` and `package.json` scripts for more details on what happens during the build. + +### Enabling Sandboxing + +[Sandboxing](#sandboxing) is highly recommended and requires, at a minimum, setting `QWEN_SANDBOX=true` in your `~/.env` and ensuring a sandboxing provider (e.g. `macOS Seatbelt`, `docker`, or `podman`) is available. See [Sandboxing](#sandboxing) for details. + +To build both the `qwen-code` CLI utility and the sandbox container, run `build:all` from the root directory: + +```bash +npm run build:all +``` + +To skip building the sandbox container, you can use `npm run build` instead. + +### Running + +To start the Qwen Code application from the source code (after building), run the following command from the root directory: + +```bash +npm start +``` + +If you'd like to run the source build outside of the qwen-code folder, you can utilize `npm link path/to/qwen-code/packages/cli` (see: [docs](https://docs.npmjs.com/cli/v9/commands/npm-link)) to run with `qwen-code` + +### Running Tests + +This project contains two types of tests: unit tests and integration tests. + +#### Unit Tests + +To execute the unit test suite for the project: + +```bash +npm run test +``` + +This will run tests located in the `packages/core` and `packages/cli` directories. Ensure tests pass before submitting any changes. For a more comprehensive check, it is recommended to run `npm run preflight`. + +#### Integration Tests + +The integration tests are designed to validate the end-to-end functionality of Qwen Code. They are not run as part of the default `npm run test` command. + +To run the integration tests, use the following command: + +```bash +npm run test:e2e +``` + +For more detailed information on the integration testing framework, please see the [Integration Tests documentation](./docs/integration-tests.md). + +### Linting and Preflight Checks + +To ensure code quality and formatting consistency, run the preflight check: + +```bash +npm run preflight +``` + +This command will run ESLint, Prettier, all tests, and other checks as defined in the project's `package.json`. + +_ProTip_ + +after cloning create a git precommit hook file to ensure your commits are always clean. + +```bash +echo " +# Run npm build and check for errors +if ! npm run preflight; then + echo "npm build failed. Commit aborted." + exit 1 +fi +" > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +``` + +#### Formatting + +To separately format the code in this project by running the following command from the root directory: + +```bash +npm run format +``` + +This command uses Prettier to format the code according to the project's style guidelines. + +#### Linting + +To separately lint the code in this project, run the following command from the root directory: + +```bash +npm run lint +``` + +### Coding Conventions + +- Please adhere to the coding style, patterns, and conventions used throughout the existing codebase. +- **Imports:** Pay special attention to import paths. The project uses ESLint to enforce restrictions on relative imports between packages. + +### Project Structure + +- `packages/`: Contains the individual sub-packages of the project. + - `cli/`: The command-line interface. + - `core/`: The core backend logic for Qwen Code. +- `docs/`: Contains all project documentation. +- `scripts/`: Utility scripts for building, testing, and development tasks. + +For more detailed architecture, see `docs/architecture.md`. + +## Documentation Development + +This section describes how to develop and preview the documentation locally. + +### Prerequisites + +1. Ensure you have Node.js (version 18+) installed +2. Have npm or yarn available + +### Setup Documentation Site Locally + +To work on the documentation and preview changes locally: + +1. Navigate to the `docs-site` directory: + + ```bash + cd docs-site + ``` + +2. Install dependencies: + + ```bash + npm install + ``` + +3. Link the documentation content from the main `docs` directory: + + ```bash + npm run link + ``` + + This creates a symbolic link from `../docs` to `content` in the docs-site project, allowing the documentation content to be served by the Next.js site. + +4. Start the development server: + + ```bash + npm run dev + ``` + +5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the documentation site with live updates as you make changes. + +Any changes made to the documentation files in the main `docs` directory will be reflected immediately in the documentation site. + +## Debugging + +### VS Code: + +0. Run the CLI to interactively debug in VS Code with `F5` +1. Start the CLI in debug mode from the root directory: + ```bash + npm run debug + ``` + This command runs `node --inspect-brk dist/index.js` within the `packages/cli` directory, pausing execution until a debugger attaches. You can then open `chrome://inspect` in your Chrome browser to connect to the debugger. +2. In VS Code, use the "Attach" launch configuration (found in `.vscode/launch.json`). + +Alternatively, you can use the "Launch Program" configuration in VS Code if you prefer to launch the currently open file directly, but 'F5' is generally recommended. + +To hit a breakpoint inside the sandbox container run: + +```bash +DEBUG=1 qwen-code +``` + +**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect qwen-code due to automatic exclusion. Use `.qwen-code/.env` files for qwen-code specific debug settings. + +### React DevTools + +To debug the CLI's React-based UI, you can use React DevTools. Ink, the library used for the CLI's interface, is compatible with React DevTools version 4.x. + +1. **Start the Qwen Code application in development mode:** + + ```bash + DEV=true npm start + ``` + +2. **Install and run React DevTools version 4.28.5 (or the latest compatible 4.x version):** + + You can either install it globally: + + ```bash + npm install -g react-devtools@4.28.5 + react-devtools + ``` + + Or run it directly using npx: + + ```bash + npx react-devtools@4.28.5 + ``` + + Your running CLI application should then connect to React DevTools. + +## Sandboxing + +> TBD + +## Manual Publish + +We publish an artifact for each commit to our internal registry. But if you need to manually cut a local build, then run the following commands: + +``` +npm run clean +npm install +npm run auth +npm run prerelease:dev +npm publish --workspaces +``` diff --git a/docs/development/_meta.ts b/docs/developers/development/_meta.ts similarity index 72% rename from docs/development/_meta.ts rename to docs/developers/development/_meta.ts index 6428e766..1a8f5b84 100644 --- a/docs/development/_meta.ts +++ b/docs/developers/development/_meta.ts @@ -1,8 +1,9 @@ export default { - architecture: 'Architecture', npm: 'NPM', - deployment: 'Deployment', telemetry: 'Telemetry', 'integration-tests': 'Integration Tests', 'issue-and-pr-automation': 'Issue and PR Automation', + deployment: { + display: 'hidden', + }, }; diff --git a/docs/development/deployment.md b/docs/developers/development/deployment.md similarity index 100% rename from docs/development/deployment.md rename to docs/developers/development/deployment.md diff --git a/docs/development/integration-tests.md b/docs/developers/development/integration-tests.md similarity index 100% rename from docs/development/integration-tests.md rename to docs/developers/development/integration-tests.md diff --git a/docs/development/issue-and-pr-automation.md b/docs/developers/development/issue-and-pr-automation.md similarity index 100% rename from docs/development/issue-and-pr-automation.md rename to docs/developers/development/issue-and-pr-automation.md diff --git a/docs/development/npm.md b/docs/developers/development/npm.md similarity index 72% rename from docs/development/npm.md rename to docs/developers/development/npm.md index 0a3c0af2..76dfb72d 100644 --- a/docs/development/npm.md +++ b/docs/developers/development/npm.md @@ -31,42 +31,57 @@ Releases are managed through the [release.yml](https://github.com/QwenLM/qwen-co - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release. 5. Click **Run workflow**. -## Nightly Releases +## Release Types -In addition to manual releases, this project has an automated nightly release process to provide the latest "bleeding edge" version for testing and development. +The project supports multiple types of releases: -### Process +### Stable Releases -Every night at midnight UTC, the [Release workflow](https://github.com/QwenLM/qwen-code/actions/workflows/release.yml) runs automatically on a schedule. It performs the following steps: +Regular stable releases for production use. -1. Checks out the latest code from the `main` branch. -2. Installs all dependencies. -3. Runs the full suite of `preflight` checks and integration tests. -4. If all tests succeed, it calculates the next nightly version number (e.g., `v0.2.1-nightly.20230101`). -5. It then builds and publishes the packages to npm with the `nightly` dist-tag. -6. Finally, it creates a GitHub Release for the nightly version. +### Preview Releases -### Failure Handling +Weekly preview releases every Tuesday at 23:59 UTC for early access to upcoming features. -If any step in the nightly workflow fails, it will automatically create a new issue in the repository with the labels `bug` and `nightly-failure`. The issue will contain a link to the failed workflow run for easy debugging. +### Nightly Releases -### How to Use the Nightly Build +Daily nightly releases at midnight UTC for bleeding-edge development testing. -To install the latest nightly build, use the `@nightly` tag: +## Automated Release Schedule + +- **Nightly**: Every day at midnight UTC +- **Preview**: Every Tuesday at 23:59 UTC +- **Stable**: Manual releases triggered by maintainers + +### How to Use Different Release Types + +To install the latest version of each type: ```bash +# Stable (default) +npm install -g @qwen-code/qwen-code + +# Preview +npm install -g @qwen-code/qwen-code@preview + +# Nightly npm install -g @qwen-code/qwen-code@nightly ``` -We also run a Google cloud build called [release-docker.yml](../.gcp/release-docker.yml). Which publishes the sandbox docker to match your release. This will also be moved to GH and combined with the main release file once service account permissions are sorted out. +### Release Process Details -### After the Release +Every scheduled or manual release follows these steps: -After the workflow has successfully completed, you can monitor its progress in the [GitHub Actions tab](https://github.com/QwenLM/qwen-code/actions/workflows/release.yml). Once complete, you should: +1. Checks out the specified code (latest from `main` branch or specific commit). +2. Installs all dependencies. +3. Runs the full suite of `preflight` checks and integration tests. +4. If all tests succeed, it calculates the appropriate version number based on release type. +5. Builds and publishes the packages to npm with the appropriate dist-tag. +6. Creates a GitHub Release for the version. -1. Go to the [pull requests page](https://github.com/QwenLM/qwen-code/pulls) of the repository. -2. Create a new pull request from the `release/vX.Y.Z` branch to `main`. -3. Review the pull request (it should only contain version updates in `package.json` files) and merge it. This keeps the version in `main` up-to-date. +### Failure Handling + +If any step in the release workflow fails, it will automatically create a new issue in the repository with the labels `bug` and a type-specific failure label (e.g., `nightly-failure`, `preview-failure`). The issue will contain a link to the failed workflow run for easy debugging. ## Release Validation @@ -155,7 +170,7 @@ By performing a dry run, you can be confident that your changes to the packaging ## Release Deep Dive The main goal of the release process is to take the source code from the packages/ directory, build it, and assemble a -clean, self-contained package in a temporary `bundle` directory at the root of the project. This `bundle` directory is what +clean, self-contained package in a temporary `dist` directory at the root of the project. This `dist` directory is what actually gets published to NPM. Here are the key stages: @@ -177,83 +192,46 @@ Stage 2: Building the Source Code - Why: The TypeScript code written during development needs to be converted into plain JavaScript that can be run by Node.js. The core package is built first as the cli package depends on it. -Stage 3: Assembling the Final Publishable Package +Stage 3: Bundling and Assembling the Final Publishable Package -This is the most critical stage where files are moved and transformed into their final state for publishing. A temporary -`bundle` folder is created at the project root to house the final package contents. +This is the most critical stage where files are moved and transformed into their final state for publishing. The process uses modern bundling techniques to create the final package. -1. The `package.json` is Transformed: - - What happens: The package.json from packages/cli/ is read, modified, and written into the root `bundle`/ directory. - - File movement: packages/cli/package.json -> (in-memory transformation) -> `bundle`/package.json - - Why: The final package.json must be different from the one used in development. Key changes include: - - Removing devDependencies. - - Removing workspace-specific "dependencies": { "@qwen-code/core": "workspace:\*" } and ensuring the core code is - bundled directly into the final JavaScript file. - - Ensuring the bin, main, and files fields point to the correct locations within the final package structure. +1. Bundle Creation: + - What happens: The prepare-package.js script creates a clean distribution package in the `dist` directory. + - Key transformations: + - Copies README.md and LICENSE to dist/ + - Copies locales folder for internationalization + - Creates a clean package.json for distribution with only necessary dependencies + - Includes runtime dependencies like tiktoken + - Maintains optional dependencies for node-pty 2. The JavaScript Bundle is Created: - What happens: The built JavaScript from both packages/core/dist and packages/cli/dist are bundled into a single, - executable JavaScript file. - - File movement: packages/cli/dist/index.js + packages/core/dist/index.js -> (bundled by esbuild) -> `bundle`/gemini.js (or a - similar name). + executable JavaScript file using esbuild. + - File location: dist/cli.js - Why: This creates a single, optimized file that contains all the necessary application code. It simplifies the package - by removing the need for the core package to be a separate dependency on NPM, as its code is now included directly. + by removing the need for complex dependency resolution at install time. 3. Static and Supporting Files are Copied: - What happens: Essential files that are not part of the source code but are required for the package to work correctly - or be well-described are copied into the `bundle` directory. + or be well-described are copied into the `dist` directory. - File movement: - - README.md -> `bundle`/README.md - - LICENSE -> `bundle`/LICENSE - - packages/cli/src/utils/\*.sb (sandbox profiles) -> `bundle`/ + - README.md -> dist/README.md + - LICENSE -> dist/LICENSE + - locales/ -> dist/locales/ + - Vendor files -> dist/vendor/ - Why: - The README.md and LICENSE are standard files that should be included in any NPM package. - - The sandbox profiles (.sb files) are critical runtime assets required for the CLI's sandboxing feature to - function. They must be located next to the final executable. + - Locales support internationalization features + - Vendor files contain necessary runtime dependencies Stage 4: Publishing to NPM -- What happens: The npm publish command is run from inside the root `bundle` directory. -- Why: By running npm publish from within the `bundle` directory, only the files we carefully assembled in Stage 3 are uploaded +- What happens: The npm publish command is run from inside the root `dist` directory. +- Why: By running npm publish from within the `dist` directory, only the files we carefully assembled in Stage 3 are uploaded to the NPM registry. This prevents any source code, test files, or development configurations from being accidentally published, resulting in a clean and minimal package for users. -Summary of File Flow - -```mermaid -graph TD - subgraph "Source Files" - A["packages/core/src/*.ts
packages/cli/src/*.ts"] - B["packages/cli/package.json"] - C["README.md
LICENSE
packages/cli/src/utils/*.sb"] - end - - subgraph "Process" - D(Build) - E(Transform) - F(Assemble) - G(Publish) - end - - subgraph "Artifacts" - H["Bundled JS"] - I["Final package.json"] - J["bundle/"] - end - - subgraph "Destination" - K["NPM Registry"] - end - - A --> D --> H - B --> E --> I - C --> F - H --> F - I --> F - F --> J - J --> G --> K -``` - This process ensures that the final published artifact is a purpose-built, clean, and efficient representation of the project, rather than a direct copy of the development workspace. diff --git a/docs/development/telemetry.md b/docs/developers/development/telemetry.md similarity index 68% rename from docs/development/telemetry.md rename to docs/developers/development/telemetry.md index 5ea185a3..f5faee40 100644 --- a/docs/development/telemetry.md +++ b/docs/developers/development/telemetry.md @@ -6,13 +6,12 @@ Learn how to enable and setup OpenTelemetry for Qwen Code. - [Key Benefits](#key-benefits) - [OpenTelemetry Integration](#opentelemetry-integration) - [Configuration](#configuration) - - [Google Cloud Telemetry](#google-cloud-telemetry) + - [Aliyun Telemetry](#aliyun-telemetry) - [Prerequisites](#prerequisites) - [Direct Export (Recommended)](#direct-export-recommended) - - [Collector-Based Export (Advanced)](#collector-based-export-advanced) - [Local Telemetry](#local-telemetry) - [File-based Output (Recommended)](#file-based-output-recommended) - - [Collector-Based Export (Advanced)](#collector-based-export-advanced-1) + - [Collector-Based Export (Advanced)](#collector-based-export-advanced) - [Logs and Metrics](#logs-and-metrics) - [Logs](#logs) - [Metrics](#metrics) @@ -35,8 +34,8 @@ Learn how to enable and setup OpenTelemetry for Qwen Code. Built on **[OpenTelemetry]** — the vendor-neutral, industry-standard observability framework — Qwen Code's observability system provides: -- **Universal Compatibility**: Export to any OpenTelemetry backend (Google - Cloud, Jaeger, Prometheus, Datadog, etc.) +- **Universal Compatibility**: Export to any OpenTelemetry backend (Aliyun, + Jaeger, Prometheus, Datadog, etc.) - **Standardized Data**: Use consistent formats and collection methods across your toolchain - **Future-Proof Integration**: Connect with existing and future observability @@ -48,18 +47,22 @@ observability framework — Qwen Code's observability system provides: ## Configuration +> [!note] +> +> **⚠️ Special Note: This feature requires corresponding code changes. This documentation is provided in advance; please refer to future code updates for actual functionality.** + All telemetry behavior is controlled through your `.qwen/settings.json` file. These settings can be overridden by environment variables or CLI flags. -| Setting | Environment Variable | CLI Flag | Description | Values | Default | -| -------------- | -------------------------------- | -------------------------------------------------------- | ------------------------------------------------- | ----------------- | ----------------------- | -| `enabled` | `GEMINI_TELEMETRY_ENABLED` | `--telemetry` / `--no-telemetry` | Enable or disable telemetry | `true`/`false` | `false` | -| `target` | `GEMINI_TELEMETRY_TARGET` | `--telemetry-target ` | Where to send telemetry data | `"gcp"`/`"local"` | `"local"` | -| `otlpEndpoint` | `GEMINI_TELEMETRY_OTLP_ENDPOINT` | `--telemetry-otlp-endpoint ` | OTLP collector endpoint | URL string | `http://localhost:4317` | -| `otlpProtocol` | `GEMINI_TELEMETRY_OTLP_PROTOCOL` | `--telemetry-otlp-protocol ` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` | -| `outfile` | `GEMINI_TELEMETRY_OUTFILE` | `--telemetry-outfile ` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - | -| `logPrompts` | `GEMINI_TELEMETRY_LOG_PROMPTS` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include prompts in telemetry logs | `true`/`false` | `true` | -| `useCollector` | `GEMINI_TELEMETRY_USE_COLLECTOR` | - | Use external OTLP collector (advanced) | `true`/`false` | `false` | +| Setting | Environment Variable | CLI Flag | Description | Values | Default | +| -------------- | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------- | ------------------ | ----------------------- | +| `enabled` | `QWEN_TELEMETRY_ENABLED` | `--telemetry` / `--no-telemetry` | Enable or disable telemetry | `true`/`false` | `false` | +| `target` | `QWEN_TELEMETRY_TARGET` | `--telemetry-target ` | Where to send telemetry data | `"qwen"`/`"local"` | `"local"` | +| `otlpEndpoint` | `QWEN_TELEMETRY_OTLP_ENDPOINT` | `--telemetry-otlp-endpoint ` | OTLP collector endpoint | URL string | `http://localhost:4317` | +| `otlpProtocol` | `QWEN_TELEMETRY_OTLP_PROTOCOL` | `--telemetry-otlp-protocol ` | OTLP transport protocol | `"grpc"`/`"http"` | `"grpc"` | +| `outfile` | `QWEN_TELEMETRY_OUTFILE` | `--telemetry-outfile ` | Save telemetry to file (overrides `otlpEndpoint`) | file path | - | +| `logPrompts` | `QWEN_TELEMETRY_LOG_PROMPTS` | `--telemetry-log-prompts` / `--no-telemetry-log-prompts` | Include prompts in telemetry logs | `true`/`false` | `true` | +| `useCollector` | `QWEN_TELEMETRY_USE_COLLECTOR` | - | Use external OTLP collector (advanced) | `true`/`false` | `false` | **Note on boolean environment variables:** For the boolean settings (`enabled`, `logPrompts`, `useCollector`), setting the corresponding environment variable to @@ -68,98 +71,23 @@ These settings can be overridden by environment variables or CLI flags. For detailed information about all configuration options, see the [Configuration Guide](./cli/configuration.md). -## Google Cloud Telemetry - -### Prerequisites - -Before using either method below, complete these steps: - -1. Set your Google Cloud project ID: - - For telemetry in a separate project from inference: - ```bash - export OTLP_GOOGLE_CLOUD_PROJECT="your-telemetry-project-id" - ``` - - For telemetry in the same project as inference: - ```bash - export GOOGLE_CLOUD_PROJECT="your-project-id" - ``` - -2. Authenticate with Google Cloud: - - If using a user account: - ```bash - gcloud auth application-default login - ``` - - If using a service account: - ```bash - export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account.json" - ``` -3. Make sure your account or service account has these IAM roles: - - Cloud Trace Agent - - Monitoring Metric Writer - - Logs Writer - -4. Enable the required Google Cloud APIs (if not already enabled): - ```bash - gcloud services enable \ - cloudtrace.googleapis.com \ - monitoring.googleapis.com \ - logging.googleapis.com \ - --project="$OTLP_GOOGLE_CLOUD_PROJECT" - ``` +## Aliyun Telemetry ### Direct Export (Recommended) -Sends telemetry directly to Google Cloud services. No collector needed. +Sends telemetry directly to Aliyun services. No collector needed. 1. Enable telemetry in your `.qwen/settings.json`: ```json { "telemetry": { "enabled": true, - "target": "gcp" + "target": "qwen" } } ``` 2. Run Qwen Code and send prompts. -3. View logs and metrics: - - Open the Google Cloud Console in your browser after sending prompts: - - Logs: https://console.cloud.google.com/logs/ - - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer - - Traces: https://console.cloud.google.com/traces/list - -### Collector-Based Export (Advanced) - -For custom processing, filtering, or routing, use an OpenTelemetry collector to -forward data to Google Cloud. - -1. Configure your `.qwen/settings.json`: - ```json - { - "telemetry": { - "enabled": true, - "target": "gcp", - "useCollector": true - } - } - ``` -2. Run the automation script: - ```bash - npm run telemetry -- --target=gcp - ``` - This will: - - Start a local OTEL collector that forwards to Google Cloud - - Configure your workspace - - Provide links to view traces, metrics, and logs in Google Cloud Console - - Save collector logs to `~/.qwen/tmp//otel/collector-gcp.log` - - Stop collector on exit (e.g. `Ctrl+C`) -3. Run Qwen Code and send prompts. -4. View logs and metrics: - - Open the Google Cloud Console in your browser after sending prompts: - - Logs: https://console.cloud.google.com/logs/ - - Metrics: https://console.cloud.google.com/monitoring/metrics-explorer - - Traces: https://console.cloud.google.com/traces/list - - Open `~/.qwen/tmp//otel/collector-gcp.log` to view local - collector logs. +3. View logs and metrics in the Aliyun Console. ## Local Telemetry diff --git a/docs/examples/proxy-script.md b/docs/developers/examples/proxy-script.md similarity index 100% rename from docs/examples/proxy-script.md rename to docs/developers/examples/proxy-script.md diff --git a/docs/extensions/extension-releasing.md b/docs/developers/extensions/extension-releasing.md similarity index 100% rename from docs/extensions/extension-releasing.md rename to docs/developers/extensions/extension-releasing.md diff --git a/docs/extensions/extension.md b/docs/developers/extensions/extension.md similarity index 100% rename from docs/extensions/extension.md rename to docs/developers/extensions/extension.md diff --git a/docs/extensions/getting-started-extensions.md b/docs/developers/extensions/getting-started-extensions.md similarity index 100% rename from docs/extensions/getting-started-extensions.md rename to docs/developers/extensions/getting-started-extensions.md diff --git a/docs/developers/roadmap.md b/docs/developers/roadmap.md new file mode 100644 index 00000000..0fb05f8a --- /dev/null +++ b/docs/developers/roadmap.md @@ -0,0 +1,74 @@ +# Qwen Code RoadMap + +> **Objective**: Catch up with Claude Code's product functionality, continuously refine details, and enhance user experience. + +| Category | Phase 1 | Phase 2 | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| User Experience | ✅ Terminal UI
✅ Support OpenAI Protocol
✅ Settings
✅ OAuth
✅ Cache Control
✅ Memory
✅ Compress
✅ Theme | Better UI
OnBoarding
LogView
✅ Session
Permission
🔄 Cross-platform Compatibility | +| Coding Workflow | ✅ Slash Commands
✅ MCP
✅ PlanMode
✅ TodoWrite
✅ SubAgent
✅ Multi Model
✅ Chat Management
✅ Tools (WebFetch, Bash, TextSearch, FileReadFile, EditFile) | 🔄 Hooks
SubAgent (enhanced)
✅ Skill
✅ Headless Mode
✅ Tools (WebSearch) | +| Building Open Capabilities | ✅ Custom Commands | ✅ QwenCode SDK
Extension | +| Integrating Community Ecosystem | | ✅ VSCode Plugin
🔄 ACP/Zed
✅ GHA | +| Administrative Capabilities | ✅ Stats
✅ Feedback | Costs
Dashboard | + +> For more details, please see the list below. + +## Features + +#### Completed Features + +| Feature | Version | Description | Category | +| ----------------------- | --------- | ------------------------------------------------------- | ------------------------------- | +| Skill | `V0.6.0` | Extensible custom AI skills | Coding Workflow | +| Github Actions | `V0.5.0` | qwen-code-action and automation | Integrating Community Ecosystem | +| VSCode Plugin | `V0.5.0` | VSCode extension plugin | Integrating Community Ecosystem | +| QwenCode SDK | `V0.4.0` | Open SDK for third-party integration | Building Open Capabilities | +| Session | `V0.4.0` | Enhanced session management | User Experience | +| i18n | `V0.3.0` | Internationalization and multilingual support | User Experience | +| Headless Mode | `V0.3.0` | Headless mode (non-interactive) | Coding Workflow | +| ACP/Zed | `V0.2.0` | ACP and Zed editor integration | Integrating Community Ecosystem | +| Terminal UI | `V0.1.0+` | Interactive terminal user interface | User Experience | +| Settings | `V0.1.0+` | Configuration management system | User Experience | +| Theme | `V0.1.0+` | Multi-theme support | User Experience | +| Support OpenAI Protocol | `V0.1.0+` | Support for OpenAI API protocol | User Experience | +| Chat Management | `V0.1.0+` | Session management (save, restore, browse) | Coding Workflow | +| MCP | `V0.1.0+` | Model Context Protocol integration | Coding Workflow | +| Multi Model | `V0.1.0+` | Multi-model support and switching | Coding Workflow | +| Slash Commands | `V0.1.0+` | Slash command system | Coding Workflow | +| Tool: Bash | `V0.1.0+` | Shell command execution tool (with is_background param) | Coding Workflow | +| Tool: FileRead/EditFile | `V0.1.0+` | File read/write and edit tools | Coding Workflow | +| Custom Commands | `V0.1.0+` | Custom command loading | Building Open Capabilities | +| Feedback | `V0.1.0+` | Feedback mechanism (/bug command) | Administrative Capabilities | +| Stats | `V0.1.0+` | Usage statistics and quota display | Administrative Capabilities | +| Memory | `V0.0.9+` | Project-level and global memory management | User Experience | +| Cache Control | `V0.0.9+` | DashScope cache control | User Experience | +| PlanMode | `V0.0.14` | Task planning mode | Coding Workflow | +| Compress | `V0.0.11` | Chat compression mechanism | User Experience | +| SubAgent | `V0.0.11` | Dedicated sub-agent system | Coding Workflow | +| TodoWrite | `V0.0.10` | Task management and progress tracking | Coding Workflow | +| Tool: TextSearch | `V0.0.8+` | Text search tool (grep, supports .qwenignore) | Coding Workflow | +| Tool: WebFetch | `V0.0.7+` | Web content fetching tool | Coding Workflow | +| Tool: WebSearch | `V0.0.7+` | Web search tool (using Tavily API) | Coding Workflow | +| OAuth | `V0.0.5+` | OAuth login authentication (Qwen OAuth) | User Experience | + +#### Features to Develop + +| Feature | Priority | Status | Description | Category | +| ---------------------------- | -------- | ----------- | --------------------------------- | --------------------------- | +| Better UI | P1 | Planned | Optimized terminal UI interaction | User Experience | +| OnBoarding | P1 | Planned | New user onboarding flow | User Experience | +| Permission | P1 | Planned | Permission system optimization | User Experience | +| Cross-platform Compatibility | P1 | In Progress | Windows/Linux/macOS compatibility | User Experience | +| LogView | P2 | Planned | Log viewing and debugging feature | User Experience | +| Hooks | P2 | In Progress | Extension hooks system | Coding Workflow | +| Extension | P2 | Planned | Extension system | Building Open Capabilities | +| Costs | P2 | Planned | Cost tracking and analysis | Administrative Capabilities | +| Dashboard | P2 | Planned | Management dashboard | Administrative Capabilities | + +#### Distinctive Features to Discuss + +| Feature | Status | Description | +| ---------------- | -------- | ----------------------------------------------------- | +| Home Spotlight | Research | Project discovery and quick launch | +| Competitive Mode | Research | Competitive mode | +| Pulse | Research | User activity pulse analysis (OpenAI Pulse reference) | +| Code Wiki | Research | Project codebase wiki/documentation system | diff --git a/docs/developers/sdk-typescript.md b/docs/developers/sdk-typescript.md new file mode 100644 index 00000000..46625e84 --- /dev/null +++ b/docs/developers/sdk-typescript.md @@ -0,0 +1,375 @@ +# Typescript SDK + +## @qwen-code/sdk + +A minimum experimental TypeScript SDK for programmatic access to Qwen Code. + +Feel free to submit a feature request/issue/PR. + +## Installation + +```bash +npm install @qwen-code/sdk +``` + +## Requirements + +- Node.js >= 20.0.0 +- [Qwen Code](https://github.com/QwenLM/qwen-code) >= 0.4.0 (stable) installed and accessible in PATH + +> **Note for nvm users**: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the `pathToQwenExecutable` option to the full path of the `qwen` binary. + +## Quick Start + +```typescript +import { query } from '@qwen-code/sdk'; + +// Single-turn query +const result = query({ + prompt: 'What files are in the current directory?', + options: { + cwd: '/path/to/project', + }, +}); + +// Iterate over messages +for await (const message of result) { + if (message.type === 'assistant') { + console.log('Assistant:', message.message.content); + } else if (message.type === 'result') { + console.log('Result:', message.result); + } +} +``` + +## API Reference + +### `query(config)` + +Creates a new query session with the Qwen Code. + +#### Parameters + +- `prompt`: `string | AsyncIterable` - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations. +- `options`: `QueryOptions` - Configuration options for the query session. + +#### QueryOptions + +| Option | Type | Default | Description | +| ------------------------ | ---------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cwd` | `string` | `process.cwd()` | The working directory for the query session. Determines the context in which file operations and commands are executed. | +| `model` | `string` | - | The AI model to use (e.g., `'qwen-max'`, `'qwen-plus'`, `'qwen-turbo'`). Takes precedence over `OPENAI_MODEL` and `QWEN_MODEL` environment variables. | +| `pathToQwenExecutable` | `string` | Auto-detected | Path to the Qwen Code executable. Supports multiple formats: `'qwen'` (native binary from PATH), `'/path/to/qwen'` (explicit path), `'/path/to/cli.js'` (Node.js bundle), `'node:/path/to/cli.js'` (force Node.js runtime), `'bun:/path/to/cli.js'` (force Bun runtime). If not provided, auto-detects from: `QWEN_CODE_CLI_PATH` env var, `~/.volta/bin/qwen`, `~/.npm-global/bin/qwen`, `/usr/local/bin/qwen`, `~/.local/bin/qwen`, `~/node_modules/.bin/qwen`, `~/.yarn/bin/qwen`. | +| `permissionMode` | `'default' \| 'plan' \| 'auto-edit' \| 'yolo'` | `'default'` | Permission mode controlling tool execution approval. See [Permission Modes](#permission-modes) for details. | +| `canUseTool` | `CanUseTool` | - | Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See [Custom Permission Handler](#custom-permission-handler). | +| `env` | `Record` | - | Environment variables to pass to the Qwen Code process. Merged with the current process environment. | +| `mcpServers` | `Record` | - | MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like `command`, `args`, `url`, `httpUrl`, etc. SDK servers use `{ type: 'sdk', name: string, instance: Server }`. | +| `abortController` | `AbortController` | - | Controller to cancel the query session. Call `abortController.abort()` to terminate the session and cleanup resources. | +| `debug` | `boolean` | `false` | Enable debug mode for verbose logging from the CLI process. | +| `maxSessionTurns` | `number` | `-1` (unlimited) | Maximum number of conversation turns before the session automatically terminates. A turn consists of a user message and an assistant response. | +| `coreTools` | `string[]` | - | Equivalent to `tool.core` in settings.json. If specified, only these tools will be available to the AI. Example: `['read_file', 'write_file', 'run_terminal_cmd']`. | +| `excludeTools` | `string[]` | - | Equivalent to `tool.exclude` in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports pattern matching: tool name (`'write_file'`), tool class (`'ShellTool'`), or shell command prefix (`'ShellTool(rm )'`). | +| `allowedTools` | `string[]` | - | Equivalent to `tool.allowed` in settings.json. Matching tools bypass `canUseTool` callback and execute automatically. Only applies when tool requires confirmation. Supports same pattern matching as `excludeTools`. | +| `authType` | `'openai' \| 'qwen-oauth'` | `'openai'` | Authentication type for the AI service. Using `'qwen-oauth'` in SDK is not recommended as credentials are stored in `~/.qwen` and may need periodic refresh. | +| `agents` | `SubagentConfig[]` | - | Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains. | +| `includePartialMessages` | `boolean` | `false` | When `true`, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response. | + +### Timeouts + +The SDK enforces the following default timeouts: + +| Timeout | Default | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `canUseTool` | 1 minute | Maximum time for `canUseTool` callback to respond. If exceeded, the tool request is auto-denied. | +| `mcpRequest` | 1 minute | Maximum time for SDK MCP tool calls to complete. | +| `controlRequest` | 1 minute | Maximum time for control operations like `initialize()`, `setModel()`, `setPermissionMode()`, and `interrupt()` to complete. | +| `streamClose` | 1 minute | Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers. | + +You can customize these timeouts via the `timeout` option: + +```typescript +const query = qwen.query('Your prompt', { + timeout: { + canUseTool: 60000, // 60 seconds for permission callback + mcpRequest: 600000, // 10 minutes for MCP tool calls + controlRequest: 60000, // 60 seconds for control requests + streamClose: 15000, // 15 seconds for stream close wait + }, +}); +``` + +### Message Types + +The SDK provides type guards to identify different message types: + +```typescript +import { + isSDKUserMessage, + isSDKAssistantMessage, + isSDKSystemMessage, + isSDKResultMessage, + isSDKPartialAssistantMessage, +} from '@qwen-code/sdk'; + +for await (const message of result) { + if (isSDKAssistantMessage(message)) { + // Handle assistant message + } else if (isSDKResultMessage(message)) { + // Handle result message + } +} +``` + +### Query Instance Methods + +The `Query` instance returned by `query()` provides several methods: + +```typescript +const q = query({ prompt: 'Hello', options: {} }); + +// Get session ID +const sessionId = q.getSessionId(); + +// Check if closed +const closed = q.isClosed(); + +// Interrupt the current operation +await q.interrupt(); + +// Change permission mode mid-session +await q.setPermissionMode('yolo'); + +// Change model mid-session +await q.setModel('qwen-max'); + +// Close the session +await q.close(); +``` + +## Permission Modes + +The SDK supports different permission modes for controlling tool execution: + +- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation. +- **`plan`**: Blocks all write tools, instructing AI to present a plan first. +- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation. +- **`yolo`**: All tools execute automatically without confirmation. + +### Permission Priority Chain + +1. `excludeTools` - Blocks tools completely +2. `permissionMode: 'plan'` - Blocks non-read-only tools +3. `permissionMode: 'yolo'` - Auto-approves all tools +4. `allowedTools` - Auto-approves matching tools +5. `canUseTool` callback - Custom approval logic +6. Default behavior - Auto-deny in SDK mode + +## Examples + +### Multi-turn Conversation + +```typescript +import { query, type SDKUserMessage } from '@qwen-code/sdk'; + +async function* generateMessages(): AsyncIterable { + yield { + type: 'user', + session_id: 'my-session', + message: { role: 'user', content: 'Create a hello.txt file' }, + parent_tool_use_id: null, + }; + + // Wait for some condition or user input + yield { + type: 'user', + session_id: 'my-session', + message: { role: 'user', content: 'Now read the file back' }, + parent_tool_use_id: null, + }; +} + +const result = query({ + prompt: generateMessages(), + options: { + permissionMode: 'auto-edit', + }, +}); + +for await (const message of result) { + console.log(message); +} +``` + +### Custom Permission Handler + +```typescript +import { query, type CanUseTool } from '@qwen-code/sdk'; + +const canUseTool: CanUseTool = async (toolName, input, { signal }) => { + // Allow all read operations + if (toolName.startsWith('read_')) { + return { behavior: 'allow', updatedInput: input }; + } + + // Prompt user for write operations (in a real app) + const userApproved = await promptUser(`Allow ${toolName}?`); + + if (userApproved) { + return { behavior: 'allow', updatedInput: input }; + } + + return { behavior: 'deny', message: 'User denied the operation' }; +}; + +const result = query({ + prompt: 'Create a new file', + options: { + canUseTool, + }, +}); +``` + +### With External MCP Servers + +```typescript +import { query } from '@qwen-code/sdk'; + +const result = query({ + prompt: 'Use the custom tool from my MCP server', + options: { + mcpServers: { + 'my-server': { + command: 'node', + args: ['path/to/mcp-server.js'], + env: { PORT: '3000' }, + }, + }, + }, +}); +``` + +### With SDK-Embedded MCP Servers + +The SDK provides `tool` and `createSdkMcpServer` to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process. + +#### `tool(name, description, inputSchema, handler)` + +Creates a tool definition with Zod schema type inference. + +| Parameter | Type | Description | +| ------------- | ---------------------------------- | ------------------------------------------------------------------------ | +| `name` | `string` | Tool name (1-64 chars, starts with letter, alphanumeric and underscores) | +| `description` | `string` | Human-readable description of what the tool does | +| `inputSchema` | `ZodRawShape` | Zod schema object defining the tool's input parameters | +| `handler` | `(args, extra) => Promise` | Async function that executes the tool and returns MCP content blocks | + +The handler must return a `CallToolResult` object with the following structure: + +```typescript +{ + content: Array< + | { type: 'text'; text: string } + | { type: 'image'; data: string; mimeType: string } + | { type: 'resource'; uri: string; mimeType?: string; text?: string } + >; + isError?: boolean; +} +``` + +#### `createSdkMcpServer(options)` + +Creates an SDK-embedded MCP server instance. + +| Option | Type | Default | Description | +| --------- | ------------------------ | --------- | ------------------------------------ | +| `name` | `string` | Required | Unique name for the MCP server | +| `version` | `string` | `'1.0.0'` | Server version | +| `tools` | `SdkMcpToolDefinition[]` | - | Array of tools created with `tool()` | + +Returns a `McpSdkServerConfigWithInstance` object that can be passed directly to the `mcpServers` option. + +#### Example + +```typescript +import { z } from 'zod'; +import { query, tool, createSdkMcpServer } from '@qwen-code/sdk'; + +// Define a tool with Zod schema +const calculatorTool = tool( + 'calculate_sum', + 'Add two numbers', + { a: z.number(), b: z.number() }, + async (args) => ({ + content: [{ type: 'text', text: String(args.a + args.b) }], + }), +); + +// Create the MCP server +const server = createSdkMcpServer({ + name: 'calculator', + tools: [calculatorTool], +}); + +// Use the server in a query +const result = query({ + prompt: 'What is 42 + 17?', + options: { + permissionMode: 'yolo', + mcpServers: { + calculator: server, + }, + }, +}); + +for await (const message of result) { + console.log(message); +} +``` + +### Abort a Query + +```typescript +import { query, isAbortError } from '@qwen-code/sdk'; + +const abortController = new AbortController(); + +const result = query({ + prompt: 'Long running task...', + options: { + abortController, + }, +}); + +// Abort after 5 seconds +setTimeout(() => abortController.abort(), 5000); + +try { + for await (const message of result) { + console.log(message); + } +} catch (error) { + if (isAbortError(error)) { + console.log('Query was aborted'); + } else { + throw error; + } +} +``` + +## Error Handling + +The SDK provides an `AbortError` class for handling aborted queries: + +```typescript +import { AbortError, isAbortError } from '@qwen-code/sdk'; + +try { + // ... query operations +} catch (error) { + if (isAbortError(error)) { + // Handle abort + } else { + // Handle other errors + } +} +``` diff --git a/docs/tools/_meta.ts b/docs/developers/tools/_meta.ts similarity index 73% rename from docs/tools/_meta.ts rename to docs/developers/tools/_meta.ts index dc18f5e0..9964b997 100644 --- a/docs/tools/_meta.ts +++ b/docs/developers/tools/_meta.ts @@ -1,9 +1,11 @@ export default { - index: 'Introduction', + introduction: 'Introduction', 'file-system': 'File System', 'multi-file': 'Multi-File Read', shell: 'Shell', 'todo-write': 'Todo Write', + task: 'Task', + 'exit-plan-mode': 'Exit Plan Mode', 'web-fetch': 'Web Fetch', 'web-search': 'Web Search', memory: 'Memory', diff --git a/docs/developers/tools/exit-plan-mode.md b/docs/developers/tools/exit-plan-mode.md new file mode 100644 index 00000000..ac28fa3a --- /dev/null +++ b/docs/developers/tools/exit-plan-mode.md @@ -0,0 +1,149 @@ +# Exit Plan Mode Tool (`exit_plan_mode`) + +This document describes the `exit_plan_mode` tool for Qwen Code. + +## Description + +Use `exit_plan_mode` when you are in plan mode and have finished presenting your implementation plan. This tool prompts the user to approve or reject the plan and transitions from planning mode to implementation mode. + +The tool is specifically designed for tasks that require planning implementation steps before writing code. It should NOT be used for research or information-gathering tasks. + +### Arguments + +`exit_plan_mode` takes one argument: + +- `plan` (string, required): The implementation plan you want to present to the user for approval. This should be a concise, markdown-formatted plan describing the implementation steps. + +## How to use `exit_plan_mode` with Qwen Code + +The Exit Plan Mode tool is part of Qwen Code's planning workflow. When you're in plan mode (typically after exploring a codebase and designing an implementation approach), you use this tool to: + +1. Present your implementation plan to the user +2. Request approval to proceed with implementation +3. Transition from plan mode to implementation mode based on user response + +The tool will prompt the user with your plan and provide options to: + +- **Proceed Once**: Approve the plan for this session only +- **Proceed Always**: Approve the plan and enable auto-approval for future edit operations +- **Cancel**: Reject the plan and remain in planning mode + +Usage: + +``` +exit_plan_mode(plan="Your detailed implementation plan here...") +``` + +## When to Use This Tool + +Use `exit_plan_mode` when: + +1. **Implementation tasks**: You are planning the implementation steps for a coding task +2. **Plan completion**: You have finished exploring and designing your implementation approach +3. **User approval needed**: You need user confirmation before proceeding with code changes +4. **Code writing tasks**: The task involves writing, modifying, or refactoring code + +### Examples of appropriate usage: + +- "Help me implement user authentication" → Use after planning the auth system implementation +- "Add a new API endpoint for user management" → Use after designing the endpoint structure +- "Refactor the database layer to use TypeORM" → Use after planning the refactoring approach + +## When NOT to Use This Tool + +Do NOT use `exit_plan_mode` for: + +1. **Research tasks**: Tasks focused on understanding or exploring existing code +2. **Information gathering**: When you're searching, reading, or analyzing without implementing +3. **Documentation tasks**: When creating documentation without code changes +4. **Analysis tasks**: When providing explanations or reviews without implementation + +### Examples of inappropriate usage: + +- "Search for and understand the implementation of vim mode in the codebase" → Don't use (research task) +- "Explain how the authentication system works" → Don't use (analysis task) +- "Find all TODO comments in the project" → Don't use (information gathering) + +## `exit_plan_mode` examples + +### Implementation planning example + +``` +exit_plan_mode(plan="## Implementation Plan for User Dashboard + +### 1. Create Dashboard Components +- Create `UserDashboard.tsx` with main dashboard layout +- Add `DashboardStats.tsx` for displaying user statistics +- Implement `ActivityFeed.tsx` for recent user activities + +### 2. Add API Integration +- Create `dashboardApi.ts` for dashboard data fetching +- Add endpoints for user stats and activity data +- Implement error handling and loading states + +### 3. Update Routing +- Add dashboard route in `App.tsx` +- Update navigation component with dashboard link +- Add route protection for authenticated users + +### 4. Testing and Documentation +- Write unit tests for dashboard components +- Add integration tests for API calls +- Update user documentation with dashboard features + +This implementation will take approximately 4-6 hours and requires changes to 8-10 files.") +``` + +### Refactoring plan example + +``` +exit_plan_mode(plan="## Database Layer Refactoring Plan + +### Phase 1: Setup and Configuration +- Install and configure TypeORM dependencies +- Create database connection configuration +- Set up entity definitions for existing models + +### Phase 2: Migration +- Create migration scripts for existing data +- Implement new repository pattern using TypeORM +- Replace raw SQL queries with TypeORM query builder + +### Phase 3: Testing and Validation +- Update all database tests to use TypeORM +- Validate data integrity after migration +- Performance testing to ensure no regressions + +This refactoring will modernize our database layer while maintaining backward compatibility.") +``` + +## User Response Handling + +After calling `exit_plan_mode`, the user can respond in several ways: + +- **Proceed Once**: The plan is approved for immediate implementation with default confirmation settings +- **Proceed Always**: The plan is approved and auto-approval is enabled for subsequent edit operations +- **Cancel**: The plan is rejected, and the system remains in plan mode for further planning + +The tool automatically adjusts the approval mode based on the user's choice, streamlining the implementation process according to user preferences. + +## Important Notes + +- **Plan mode only**: This tool should only be used when you are currently in plan mode +- **Implementation focus**: Only use for tasks that involve writing or modifying code +- **Concise plans**: Keep plans focused and concise - aim for clarity over exhaustive detail +- **Markdown support**: Plans support markdown formatting for better readability +- **Single use**: The tool should be used once per planning session when ready to proceed +- **User control**: The final decision to proceed always rests with the user + +## Integration with Planning Workflow + +The Exit Plan Mode tool is part of a larger planning workflow: + +1. **Enter Plan Mode**: User requests or system determines planning is needed +2. **Exploration Phase**: Analyze codebase, understand requirements, explore options +3. **Plan Design**: Create implementation strategy based on exploration +4. **Plan Presentation**: Use `exit_plan_mode` to present plan to user +5. **Implementation Phase**: Upon approval, proceed with planned implementation + +This workflow ensures thoughtful implementation approaches and gives users control over significant code changes. diff --git a/docs/tools/file-system.md b/docs/developers/tools/file-system.md similarity index 96% rename from docs/tools/file-system.md rename to docs/developers/tools/file-system.md index 3c5097df..bfa6de8d 100644 --- a/docs/tools/file-system.md +++ b/docs/developers/tools/file-system.md @@ -83,7 +83,7 @@ Qwen Code provides a comprehensive suite of tools for interacting with the local - **Tool name:** `grep_search` - **Display name:** Grep -- **File:** `ripGrep.ts` (with `grep.ts` as fallback) +- **File:** `grep.ts` (with `ripGrep.ts` as fallback) - **Parameters:** - `pattern` (string, required): The regular expression pattern to search for in file contents (e.g., `"function\\s+myFunction"`, `"log.*Error"`). - `path` (string, optional): File or directory to search in. Defaults to current working directory. @@ -141,7 +141,7 @@ grep_search(pattern="function", glob="*.js", limit=10) - `file_path` (string, required): The absolute path to the file to modify. - `old_string` (string, required): The exact literal text to replace. - **CRITICAL:** This string must uniquely identify the single instance to change. It should include at least 3 lines of context _before_ and _after_ the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content. + **CRITICAL:** This string must uniquely identify the single instance to change. It should include sufficient context around the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content. - `new_string` (string, required): The exact literal text to replace `old_string` with. - `replace_all` (boolean, optional): Replace all occurrences of `old_string`. Defaults to `false`. diff --git a/docs/tools/index.md b/docs/developers/tools/introduction.md similarity index 86% rename from docs/tools/index.md rename to docs/developers/tools/introduction.md index 6798ec6c..9c732555 100644 --- a/docs/tools/index.md +++ b/docs/developers/tools/introduction.md @@ -50,7 +50,13 @@ Qwen Code's built-in tools can be broadly categorized as follows: - **[Multi-File Read Tool](./multi-file.md) (`read_many_files`):** A specialized tool for reading content from multiple files or directories, often used by the `@` command. - **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling information across sessions. - **[Todo Write Tool](./todo-write.md) (`todo_write`):** For creating and managing structured task lists during coding sessions. +- **[Task Tool](./task.md) (`task`):** For delegating complex tasks to specialized subagents. +- **[Exit Plan Mode Tool](./exit-plan-mode.md) (`exit_plan_mode`):** For exiting plan mode and proceeding with implementation. Additionally, these tools incorporate: - **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the model and your local environment or other services like APIs. + - **[MCP Quick Start Guide](../mcp-quick-start.md)**: Get started with MCP in 5 minutes with practical examples + - **[MCP Example Configurations](../mcp-example-configs.md)**: Ready-to-use configurations for common scenarios + - **[MCP Testing & Validation](../mcp-testing-validation.md)**: Test and validate your MCP server setups +- **[Sandboxing](../sandbox.md)**: Sandboxing isolates the model and its changes from your environment to reduce potential risk. diff --git a/docs/tools/mcp-server.md b/docs/developers/tools/mcp-server.md similarity index 100% rename from docs/tools/mcp-server.md rename to docs/developers/tools/mcp-server.md diff --git a/docs/tools/memory.md b/docs/developers/tools/memory.md similarity index 100% rename from docs/tools/memory.md rename to docs/developers/tools/memory.md diff --git a/docs/tools/multi-file.md b/docs/developers/tools/multi-file.md similarity index 100% rename from docs/tools/multi-file.md rename to docs/developers/tools/multi-file.md diff --git a/docs/tools/shell.md b/docs/developers/tools/shell.md similarity index 100% rename from docs/tools/shell.md rename to docs/developers/tools/shell.md diff --git a/docs/developers/tools/task.md b/docs/developers/tools/task.md new file mode 100644 index 00000000..13850188 --- /dev/null +++ b/docs/developers/tools/task.md @@ -0,0 +1,145 @@ +# Task Tool (`task`) + +This document describes the `task` tool for Qwen Code. + +## Description + +Use `task` to launch a specialized subagent to handle complex, multi-step tasks autonomously. The Task tool delegates work to specialized agents that can work independently with access to their own set of tools, allowing for parallel task execution and specialized expertise. + +### Arguments + +`task` takes the following arguments: + +- `description` (string, required): A short (3-5 word) description of the task for user visibility and tracking purposes. +- `prompt` (string, required): The detailed task prompt for the subagent to execute. Should contain comprehensive instructions for autonomous execution. +- `subagent_type` (string, required): The type of specialized agent to use for this task. Must match one of the available configured subagents. + +## How to use `task` with Qwen Code + +The Task tool dynamically loads available subagents from your configuration and delegates tasks to them. Each subagent runs independently and can use its own set of tools, allowing for specialized expertise and parallel execution. + +When you use the Task tool, the subagent will: + +1. Receive the task prompt with full autonomy +2. Execute the task using its available tools +3. Return a final result message +4. Terminate (subagents are stateless and single-use) + +Usage: + +``` +task(description="Brief task description", prompt="Detailed task instructions for the subagent", subagent_type="agent_name") +``` + +## Available Subagents + +The available subagents depend on your configuration. Common subagent types might include: + +- **general-purpose**: For complex multi-step tasks requiring various tools +- **code-reviewer**: For reviewing and analyzing code quality +- **test-runner**: For running tests and analyzing results +- **documentation-writer**: For creating and updating documentation + +You can view available subagents by using the `/agents` command in Qwen Code. + +## Task Tool Features + +### Real-time Progress Updates + +The Task tool provides live updates showing: + +- Subagent execution status +- Individual tool calls being made by the subagent +- Tool call results and any errors +- Overall task progress and completion status + +### Parallel Execution + +You can launch multiple subagents concurrently by calling the Task tool multiple times in a single message, allowing for parallel task execution and improved efficiency. + +### Specialized Expertise + +Each subagent can be configured with: + +- Specific tool access permissions +- Specialized system prompts and instructions +- Custom model configurations +- Domain-specific knowledge and capabilities + +## `task` examples + +### Delegating to a general-purpose agent + +``` +task( + description="Code refactoring", + prompt="Please refactor the authentication module in src/auth/ to use modern async/await patterns instead of callbacks. Ensure all tests still pass and update any related documentation.", + subagent_type="general-purpose" +) +``` + +### Running parallel tasks + +``` +# Launch code review and test execution in parallel +task( + description="Code review", + prompt="Review the recent changes in the user management module for code quality, security issues, and best practices compliance.", + subagent_type="code-reviewer" +) + +task( + description="Run tests", + prompt="Execute the full test suite and analyze any failures. Provide a summary of test coverage and recommendations for improvement.", + subagent_type="test-runner" +) +``` + +### Documentation generation + +``` +task( + description="Update docs", + prompt="Generate comprehensive API documentation for the newly implemented REST endpoints in the orders module. Include request/response examples and error codes.", + subagent_type="documentation-writer" +) +``` + +## When to Use the Task Tool + +Use the Task tool when: + +1. **Complex multi-step tasks** - Tasks requiring multiple operations that can be handled autonomously +2. **Specialized expertise** - Tasks that benefit from domain-specific knowledge or tools +3. **Parallel execution** - When you have multiple independent tasks that can run simultaneously +4. **Delegation needs** - When you want to hand off a complete task rather than micromanaging steps +5. **Resource-intensive operations** - Tasks that might take significant time or computational resources + +## When NOT to Use the Task Tool + +Don't use the Task tool for: + +- **Simple, single-step operations** - Use direct tools like Read, Edit, etc. +- **Interactive tasks** - Tasks requiring back-and-forth communication +- **Specific file reads** - Use Read tool directly for better performance +- **Simple searches** - Use Grep or Glob tools directly + +## Important Notes + +- **Stateless execution**: Each subagent invocation is independent with no memory of previous executions +- **Single communication**: Subagents provide one final result message - no ongoing communication +- **Comprehensive prompts**: Your prompt should contain all necessary context and instructions for autonomous execution +- **Tool access**: Subagents only have access to tools configured in their specific configuration +- **Parallel capability**: Multiple subagents can run simultaneously for improved efficiency +- **Configuration dependent**: Available subagent types depend on your system configuration + +## Configuration + +Subagents are configured through Qwen Code's agent configuration system. Use the `/agents` command to: + +- View available subagents +- Create new subagent configurations +- Modify existing subagent settings +- Set tool permissions and capabilities + +For more information on configuring subagents, refer to the subagents documentation. diff --git a/docs/tools/todo-write.md b/docs/developers/tools/todo-write.md similarity index 84% rename from docs/tools/todo-write.md rename to docs/developers/tools/todo-write.md index 5da90b12..157e3659 100644 --- a/docs/tools/todo-write.md +++ b/docs/developers/tools/todo-write.md @@ -11,9 +11,9 @@ Use `todo_write` to create and manage a structured task list for your current co `todo_write` takes one argument: - `todos` (array, required): An array of todo items, where each item contains: - - `id` (string, required): A unique identifier for the todo item. - `content` (string, required): The description of the task. - `status` (string, required): The current status (`pending`, `in_progress`, or `completed`). + - `activeForm` (string, required): The present continuous form describing what is being done (e.g., "Running tests", "Building the project"). ## How to use `todo_write` with Qwen Code @@ -39,19 +39,19 @@ Creating a feature implementation plan: ``` todo_write(todos=[ { - "id": "create-model", "content": "Create user preferences model", - "status": "pending" + "status": "pending", + "activeForm": "Creating user preferences model" }, { - "id": "add-endpoints", "content": "Add API endpoints for preferences", - "status": "pending" + "status": "pending", + "activeForm": "Adding API endpoints for preferences" }, { - "id": "implement-ui", "content": "Implement frontend components", - "status": "pending" + "status": "pending", + "activeForm": "Implementing frontend components" } ]) ``` diff --git a/docs/tools/web-fetch.md b/docs/developers/tools/web-fetch.md similarity index 100% rename from docs/tools/web-fetch.md rename to docs/developers/tools/web-fetch.md diff --git a/docs/tools/web-search.md b/docs/developers/tools/web-search.md similarity index 100% rename from docs/tools/web-search.md rename to docs/developers/tools/web-search.md diff --git a/docs/development/architecture.md b/docs/development/architecture.md deleted file mode 100644 index f970cba3..00000000 --- a/docs/development/architecture.md +++ /dev/null @@ -1,54 +0,0 @@ -# Qwen Code Architecture Overview - -This document provides a high-level overview of Qwen Code's architecture. - -## Core components - -Qwen Code is primarily composed of two main packages, along with a suite of tools that can be used by the system in the course of handling command-line input: - -1. **CLI package (`packages/cli`):** - - **Purpose:** This contains the user-facing portion of Qwen Code, such as handling the initial user input, presenting the final output, and managing the overall user experience. - - **Key functions contained in the package:** - - [Input processing](./cli/commands.md) - - History management - - Display rendering - - [Theme and UI customization](./cli/themes.md) - - [CLI configuration settings](./cli/configuration.md) - -2. **Core package (`packages/core`):** - - **Purpose:** This acts as the backend for Qwen Code. It receives requests sent from `packages/cli`, orchestrates interactions with the configured model API, and manages the execution of available tools. - - **Key functions contained in the package:** - - API client for communicating with the Google Gemini API - - Prompt construction and management - - Tool registration and execution logic - - State management for conversations or sessions - - Server-side configuration - -3. **Tools (`packages/core/src/tools/`):** - - **Purpose:** These are individual modules that extend the capabilities of the Gemini model, allowing it to interact with the local environment (e.g., file system, shell commands, web fetching). - - **Interaction:** `packages/core` invokes these tools based on requests from the Gemini model. - -## Interaction Flow - -A typical interaction with Qwen Code follows this flow: - -1. **User input:** The user types a prompt or command into the terminal, which is managed by `packages/cli`. -2. **Request to core:** `packages/cli` sends the user's input to `packages/core`. -3. **Request processed:** The core package: - - Constructs an appropriate prompt for the configured model API, possibly including conversation history and available tool definitions. - - Sends the prompt to the model API. -4. **Model API response:** The model API processes the prompt and returns a response. This response might be a direct answer or a request to use one of the available tools. -5. **Tool execution (if applicable):** - - When the model API requests a tool, the core package prepares to execute it. - - If the requested tool can modify the file system or execute shell commands, the user is first given details of the tool and its arguments, and the user must approve the execution. - - Read-only operations, such as reading files, might not require explicit user confirmation to proceed. - - Once confirmed, or if confirmation is not required, the core package executes the relevant action within the relevant tool, and the result is sent back to the model API by the core package. - - The model API processes the tool result and generates a final response. -6. **Response to CLI:** The core package sends the final response back to the CLI package. -7. **Display to user:** The CLI package formats and displays the response to the user in the terminal. - -## Key Design Principles - -- **Modularity:** Separating the CLI (frontend) from the Core (backend) allows for independent development and potential future extensions (e.g., different frontends for the same backend). -- **Extensibility:** The tool system is designed to be extensible, allowing new capabilities to be added. -- **User experience:** The CLI focuses on providing a rich and interactive terminal experience. diff --git a/docs/features/_meta.ts b/docs/features/_meta.ts deleted file mode 100644 index 7ad3361c..00000000 --- a/docs/features/_meta.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default { - subagents: 'Subagents', - checkpointing: 'Checkpointing', - sandbox: 'Sandbox Support', - headless: 'Headless Mode', - 'welcome-back': 'Welcome Back', - 'token-caching': 'Token Caching', -}; diff --git a/docs/features/sandbox.md b/docs/features/sandbox.md deleted file mode 100644 index f67ddae6..00000000 --- a/docs/features/sandbox.md +++ /dev/null @@ -1,157 +0,0 @@ -# Sandboxing in Qwen Code - -This document provides a guide to sandboxing in Qwen Code, including prerequisites, quickstart, and configuration. - -## Prerequisites - -Before using sandboxing, you need to install and set up Qwen Code: - -```bash -npm install -g @qwen-code/qwen-code -``` - -To verify the installation - -```bash -qwen --version -``` - -## Overview of sandboxing - -Sandboxing isolates potentially dangerous operations (such as shell commands or file modifications) from your host system, providing a security barrier between AI operations and your environment. - -The benefits of sandboxing include: - -- **Security**: Prevent accidental system damage or data loss. -- **Isolation**: Limit file system access to project directory. -- **Consistency**: Ensure reproducible environments across different systems. -- **Safety**: Reduce risk when working with untrusted code or experimental commands. - -## Sandboxing methods - -Your ideal method of sandboxing may differ depending on your platform and your preferred container solution. - -### 1. macOS Seatbelt (macOS only) - -Lightweight, built-in sandboxing using `sandbox-exec`. - -**Default profile**: `permissive-open` - restricts writes outside project directory but allows most other operations. - -### 2. Container-based (Docker/Podman) - -Cross-platform sandboxing with complete process isolation. - -**Note**: Requires building the sandbox image locally or using a published image from your organization's registry. - -## Quickstart - -```bash -# Enable sandboxing with command flag -qwen -s -p "analyze the code structure" - -# Use environment variable -export GEMINI_SANDBOX=true -qwen -p "run the test suite" - -# Configure in settings.json -{ - "tools": { - "sandbox": "docker" - } -} -``` - -## Configuration - -### Enable sandboxing (in order of precedence) - -1. **Command flag**: `-s` or `--sandbox` -2. **Environment variable**: `GEMINI_SANDBOX=true|docker|podman|sandbox-exec` -3. **Settings file**: `"sandbox": true` in the `tools` object of your `settings.json` file (e.g., `{"tools": {"sandbox": true}}`). - -### macOS Seatbelt profiles - -Built-in profiles (set via `SEATBELT_PROFILE` env var): - -- `permissive-open` (default): Write restrictions, network allowed -- `permissive-closed`: Write restrictions, no network -- `permissive-proxied`: Write restrictions, network via proxy -- `restrictive-open`: Strict restrictions, network allowed -- `restrictive-closed`: Maximum restrictions - -### Custom Sandbox Flags - -For container-based sandboxing, you can inject custom flags into the `docker` or `podman` command using the `SANDBOX_FLAGS` environment variable. This is useful for advanced configurations, such as disabling security features for specific use cases. - -**Example (Podman)**: - -To disable SELinux labeling for volume mounts, you can set the following: - -```bash -export SANDBOX_FLAGS="--security-opt label=disable" -``` - -Multiple flags can be provided as a space-separated string: - -```bash -export SANDBOX_FLAGS="--flag1 --flag2=value" -``` - -## Linux UID/GID handling - -The sandbox automatically handles user permissions on Linux. Override these permissions with: - -```bash -export SANDBOX_SET_UID_GID=true # Force host UID/GID -export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping -``` - -## Troubleshooting - -### Common issues - -**"Operation not permitted"** - -- Operation requires access outside sandbox. -- Try more permissive profile or add mount points. - -**Missing commands** - -- Add to custom Dockerfile. -- Install via `sandbox.bashrc`. - -**Network issues** - -- Check sandbox profile allows network. -- Verify proxy configuration. - -### Debug mode - -```bash -DEBUG=1 qwen -s -p "debug command" -``` - -**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect the CLI due to automatic exclusion. Use `.qwen/.env` files for Qwen Code-specific debug settings. - -### Inspect sandbox - -```bash -# Check environment -qwen -s -p "run shell command: env | grep SANDBOX" - -# List mounts -qwen -s -p "run shell command: mount | grep workspace" -``` - -## Security notes - -- Sandboxing reduces but doesn't eliminate all risks. -- Use the most restrictive profile that allows your work. -- Container overhead is minimal after first build. -- GUI applications may not work in sandboxes. - -## Related documentation - -- [Configuration](./cli/configuration.md): Full configuration options. -- [Commands](./cli/commands.md): Available commands. -- [Troubleshooting](./troubleshooting.md): General troubleshooting. diff --git a/docs/features/token-caching.md b/docs/features/token-caching.md deleted file mode 100644 index 8d6dc5ec..00000000 --- a/docs/features/token-caching.md +++ /dev/null @@ -1,14 +0,0 @@ -# Token Caching and Cost Optimization - -Qwen Code automatically optimizes API costs through token caching when using API key authentication (e.g., OpenAI-compatible providers). This feature reuses previous system instructions and context to reduce the number of tokens processed in subsequent requests. - -**Token caching is available for:** - -- API key users (Qwen API key) -- Vertex AI users (with project and location setup) - -**Token caching is not available for:** - -- OAuth users (Google Personal/Enterprise accounts) - the Code Assist API does not support cached content creation at this time - -You can view your token usage and cached token savings using the `/stats` command. When cached tokens are available, they will be displayed in the stats output. diff --git a/docs/features/welcome-back.md b/docs/features/welcome-back.md deleted file mode 100644 index 1ce552ee..00000000 --- a/docs/features/welcome-back.md +++ /dev/null @@ -1,125 +0,0 @@ -# Welcome Back Feature - -The Welcome Back feature helps you seamlessly resume your work by automatically detecting when you return to a project with existing conversation history and offering to continue from where you left off. - -## Overview - -When you start Qwen Code in a project directory that contains a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`), the Welcome Back dialog will automatically appear, giving you the option to either start fresh or continue your previous conversation. - -## How It Works - -### Automatic Detection - -The Welcome Back feature automatically detects: - -- **Project Summary File:** Looks for `.qwen/PROJECT_SUMMARY.md` in your current project directory -- **Conversation History:** Checks if there's meaningful conversation history to resume -- **Settings:** Respects your `enableWelcomeBack` setting (enabled by default) - -### Welcome Back Dialog - -When a project summary is found, you'll see a dialog with: - -- **Last Updated Time:** Shows when the summary was last generated -- **Overall Goal:** Displays the main objective from your previous session -- **Current Plan:** Shows task progress with status indicators: - - `[DONE]` - Completed tasks - - `[IN PROGRESS]` - Currently working on - - `[TODO]` - Planned tasks -- **Task Statistics:** Summary of total tasks, completed, in progress, and pending - -### Options - -You have two choices when the Welcome Back dialog appears: - -1. **Start new chat session** - - Closes the dialog and begins a fresh conversation - - No previous context is loaded - -2. **Continue previous conversation** - - Automatically fills the input with: `@.qwen/PROJECT_SUMMARY.md, Based on our previous conversation, Let's continue?` - - Loads the project summary as context for the AI - - Allows you to seamlessly pick up where you left off - -## Configuration - -### Enable/Disable Welcome Back - -You can control the Welcome Back feature through settings: - -**Via Settings Dialog:** - -1. Run `/settings` in Qwen Code -2. Find "Enable Welcome Back" in the UI category -3. Toggle the setting on/off - -**Via Settings File:** -Add to your `.qwen/settings.json`: - -```json -{ - "enableWelcomeBack": true -} -``` - -**Settings Locations:** - -- **User settings:** `~/.qwen/settings.json` (affects all projects) -- **Project settings:** `.qwen/settings.json` (project-specific) - -### Keyboard Shortcuts - -- **Escape:** Close the Welcome Back dialog (defaults to "Start new chat session") - -## Integration with Other Features - -### Project Summary Generation - -The Welcome Back feature works seamlessly with the `/summary` command: - -1. **Generate Summary:** Use `/summary` to create a project summary -2. **Automatic Detection:** Next time you start Qwen Code in this project, Welcome Back will detect the summary -3. **Resume Work:** Choose to continue and the summary will be loaded as context - -## File Structure - -The Welcome Back feature creates and uses: - -``` -your-project/ -├── .qwen/ -│ └── PROJECT_SUMMARY.md # Generated project summary -``` - -### PROJECT_SUMMARY.md Format - -The generated summary follows this structure: - -```markdown -# Project Summary - -## Overall Goal - - - -## Key Knowledge - - - - -## Recent Actions - - - - -## Current Plan - - - - ---- - -## Summary Metadata - -**Update time**: 2025-01-10T15:30:00.000Z -``` diff --git a/docs/index.md b/docs/index.md index 07fc1db6..802c6899 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,344 +1,25 @@ -# Welcome to Qwen Code documentation +# Qwen Code Documentation -Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) ([details](./README.gemini.md)), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance. +Welcome to the Qwen Code documentation. Qwen Code is an agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before. -## 🚀 Why Choose Qwen Code? +## Documentation Sections -- 🎯 **Free Tier:** Up to 60 requests/min and 2,000 requests/day with your [QwenChat](https://chat.qwen.ai/) account. -- 🧠 **Advanced Model:** Specially optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) for superior code understanding and assistance. -- 🏆 **Comprehensive Features:** Includes subagents, Plan Mode, TodoWrite, vision model support, and full OpenAI API compatibility—all seamlessly integrated. -- 🔧 **Built-in & Extensible Tools:** Includes file system operations, shell command execution, web fetch/search, and more—all easily extended via the Model Context Protocol (MCP) for custom integrations. -- 💻 **Developer-Centric:** Built for terminal-first workflows—perfect for command-line enthusiasts. -- 🛡️ **Open Source:** Apache 2.0 licensed for maximum freedom and transparency. +### [User Guide](./users/overview) -## Installation +Learn how to use Qwen Code as an end user. This section covers: -### Prerequisites +- Basic installation and setup +- Common usage patterns +- Features and capabilities +- Configuration options +- Troubleshooting -Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed. +### [Developer Guide](./developers/contributing) -```bash -curl -qL https://www.npmjs.com/install.sh | sh -``` +Learn how to contribute to and develop Qwen Code. This section covers: -### Install from npm - -```bash -npm install -g @qwen-code/qwen-code@latest -qwen --version -``` - -### Install from source - -```bash -git clone https://github.com/QwenLM/qwen-code.git -cd qwen-code -npm install -npm install -g . -``` - -### Install globally with Homebrew (macOS/Linux) - -```bash -brew install qwen-code -``` - -## Quick Start - -```bash -# Start Qwen Code -qwen - -# Example commands -> Explain this codebase structure -> Help me refactor this function -> Generate unit tests for this module -``` - -### Session Management - -Control your token usage with configurable session limits to optimize costs and performance. - -#### Configure Session Token Limit - -Create or edit `.qwen/settings.json` in your home directory: - -```json -{ - "sessionTokenLimit": 32000 -} -``` - -#### Session Commands - -- **`/compress`** - Compress conversation history to continue within token limits -- **`/clear`** (aliases: `/reset`, `/new`) - Clear conversation history, start a fresh session, and free up context -- **`/stats`** - Check current token usage and limits - -> 📝 **Note**: Session token limit applies to a single conversation, not cumulative API calls. - -### Vision Model Configuration - -Qwen Code includes intelligent vision model auto-switching that detects images in your input and can automatically switch to vision-capable models for multimodal analysis. **This feature is enabled by default** - when you include images in your queries, you'll see a dialog asking how you'd like to handle the vision model switch. - -#### Skip the Switch Dialog (Optional) - -If you don't want to see the interactive dialog each time, configure the default behavior in your `.qwen/settings.json`: - -```json -{ - "experimental": { - "vlmSwitchMode": "once" - } -} -``` - -**Available modes:** - -- **`"once"`** - Switch to vision model for this query only, then revert -- **`"session"`** - Switch to vision model for the entire session -- **`"persist"`** - Continue with current model (no switching) -- **Not set** - Show interactive dialog each time (default) - -#### Command Line Override - -You can also set the behavior via command line: - -```bash -# Switch once per query -qwen --vlm-switch-mode once - -# Switch for entire session -qwen --vlm-switch-mode session - -# Never switch automatically -qwen --vlm-switch-mode persist -``` - -#### Disable Vision Models (Optional) - -To completely disable vision model support, add to your `.qwen/settings.json`: - -```json -{ - "experimental": { - "visionModelPreview": false - } -} -``` - -> 💡 **Tip**: In YOLO mode (`--yolo`), vision switching happens automatically without prompts when images are detected. - -### Authorization - -Choose your preferred authentication method based on your needs: - -#### 1. Qwen OAuth (🚀 Recommended - Start in 30 seconds) - -The easiest way to get started - completely free with generous quotas: - -```bash -# Just run this command and follow the browser authentication -qwen -``` - -**What happens:** - -1. **Instant Setup**: CLI opens your browser automatically -2. **One-Click Login**: Authenticate with your qwen.ai account -3. **Automatic Management**: Credentials cached locally for future use -4. **No Configuration**: Zero setup required - just start coding! - -**Free Tier Benefits:** - -- ✅ **2,000 requests/day** (no token counting needed) -- ✅ **60 requests/minute** rate limit -- ✅ **Automatic credential refresh** -- ✅ **Zero cost** for individual users -- ℹ️ **Note**: Model fallback may occur to maintain service quality - -#### 2. OpenAI-Compatible API - -Use API keys for OpenAI or other compatible providers: - -**Configuration Methods:** - -1. **Environment Variables** - - ```bash - export OPENAI_API_KEY="your_api_key_here" - export OPENAI_BASE_URL="your_api_endpoint" - export OPENAI_MODEL="your_model_choice" - ``` - -2. **Project `.env` File** - Create a `.env` file in your project root: - ```env - OPENAI_API_KEY=your_api_key_here - OPENAI_BASE_URL=your_api_endpoint - OPENAI_MODEL=your_model_choice - ``` - -**API Provider Options** - -> ⚠️ **Regional Notice:** -> -> - **Mainland China**: Use Alibaba Cloud Bailian or ModelScope -> - **International**: Use Alibaba Cloud ModelStudio or OpenRouter - -
-🇨🇳 For Users in Mainland China - -**Option 1: Alibaba Cloud Bailian** ([Apply for API Key](https://bailian.console.aliyun.com/)) - -```bash -export OPENAI_API_KEY="your_api_key_here" -export OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" -export OPENAI_MODEL="qwen3-coder-plus" -``` - -**Option 2: ModelScope (Free Tier)** ([Apply for API Key](https://modelscope.cn/docs/model-service/API-Inference/intro)) - -- ✅ **2,000 free API calls per day** -- ⚠️ Connect your Aliyun account to avoid authentication errors - -```bash -export OPENAI_API_KEY="your_api_key_here" -export OPENAI_BASE_URL="https://api-inference.modelscope.cn/v1" -export OPENAI_MODEL="Qwen/Qwen3-Coder-480B-A35B-Instruct" -``` - -
- -
-🌍 For International Users - -**Option 1: Alibaba Cloud ModelStudio** ([Apply for API Key](https://modelstudio.console.alibabacloud.com/)) - -```bash -export OPENAI_API_KEY="your_api_key_here" -export OPENAI_BASE_URL="https://dashscope-intl.aliyuncs.com/compatible-mode/v1" -export OPENAI_MODEL="qwen3-coder-plus" -``` - -**Option 2: OpenRouter (Free Tier Available)** ([Apply for API Key](https://openrouter.ai/)) - -```bash -export OPENAI_API_KEY="your_api_key_here" -export OPENAI_BASE_URL="https://openrouter.ai/api/v1" -export OPENAI_MODEL="qwen/qwen3-coder:free" -``` - -
- -## Usage Examples - -### 🔍 Explore Codebases - -```bash -cd your-project/ -qwen - -# Architecture analysis -> Describe the main pieces of this system's architecture -> What are the key dependencies and how do they interact? -> Find all API endpoints and their authentication methods -``` - -### 💻 Code Development - -```bash -# Refactoring -> Refactor this function to improve readability and performance -> Convert this class to use dependency injection -> Split this large module into smaller, focused components - -# Code generation -> Create a REST API endpoint for user management -> Generate unit tests for the authentication module -> Add error handling to all database operations -``` - -### 🔄 Automate Workflows - -```bash -# Git automation -> Analyze git commits from the last 7 days, grouped by feature -> Create a changelog from recent commits -> Find all TODO comments and create GitHub issues - -# File operations -> Convert all images in this directory to PNG format -> Rename all test files to follow the *.test.ts pattern -> Find and remove all console.log statements -``` - -### 🐛 Debugging & Analysis - -```bash -# Performance analysis -> Identify performance bottlenecks in this React component -> Find all N+1 query problems in the codebase - -# Security audit -> Check for potential SQL injection vulnerabilities -> Find all hardcoded credentials or API keys -``` - -## Popular Tasks - -### 📚 Understand New Codebases - -```text -> What are the core business logic components? -> What security mechanisms are in place? -> How does the data flow through the system? -> What are the main design patterns used? -> Generate a dependency graph for this module -``` - -### 🔨 Code Refactoring & Optimization - -```text -> What parts of this module can be optimized? -> Help me refactor this class to follow SOLID principles -> Add proper error handling and logging -> Convert callbacks to async/await pattern -> Implement caching for expensive operations -``` - -### 📝 Documentation & Testing - -```text -> Generate comprehensive JSDoc comments for all public APIs -> Write unit tests with edge cases for this component -> Create API documentation in OpenAPI format -> Add inline comments explaining complex algorithms -> Generate a README for this module -``` - -### 🚀 Development Acceleration - -```text -> Set up a new Express server with authentication -> Create a React component with TypeScript and tests -> Implement a rate limiter middleware -> Add database migrations for new schema -> Configure CI/CD pipeline for this project -``` - -## Commands & Shortcuts - -### Session Commands - -- `/help` - Display available commands -- `/clear` (aliases: `/reset`, `/new`) - Clear conversation history and start a fresh session -- `/compress` - Compress history to save tokens -- `/stats` - Show current session information -- `/exit` or `/quit` - Exit Qwen Code - -### Keyboard Shortcuts - -- `Ctrl+C` - Cancel current operation -- `Ctrl+D` - Exit (on empty line) -- `Up/Down` - Navigate command history +- Architecture overview +- Contributing guidelines +- Core concepts and implementation details +- Tools and development workflow +- Extension and plugin development diff --git a/docs/mermaid/context.mmd b/docs/mermaid/context.mmd deleted file mode 100644 index ebe4fbee..00000000 --- a/docs/mermaid/context.mmd +++ /dev/null @@ -1,103 +0,0 @@ -graph LR - %% --- Style Definitions --- - classDef new fill:#98fb98,color:#000 - classDef changed fill:#add8e6,color:#000 - classDef unchanged fill:#f0f0f0,color:#000 - - %% --- Subgraphs --- - subgraph "Context Providers" - direction TB - A["gemini.tsx"] - B["AppContainer.tsx"] - end - - subgraph "Contexts" - direction TB - CtxSession["SessionContext"] - CtxVim["VimModeContext"] - CtxSettings["SettingsContext"] - CtxApp["AppContext"] - CtxConfig["ConfigContext"] - CtxUIState["UIStateContext"] - CtxUIActions["UIActionsContext"] - end - - subgraph "Component Consumers" - direction TB - ConsumerApp["App"] - ConsumerAppContainer["AppContainer"] - ConsumerAppHeader["AppHeader"] - ConsumerDialogManager["DialogManager"] - ConsumerHistoryItem["HistoryItemDisplay"] - ConsumerComposer["Composer"] - ConsumerMainContent["MainContent"] - ConsumerNotifications["Notifications"] - end - - %% --- Provider -> Context Connections --- - A -.-> CtxSession - A -.-> CtxVim - A -.-> CtxSettings - - B -.-> CtxApp - B -.-> CtxConfig - B -.-> CtxUIState - B -.-> CtxUIActions - B -.-> CtxSettings - - %% --- Context -> Consumer Connections --- - CtxSession -.-> ConsumerAppContainer - CtxSession -.-> ConsumerApp - - CtxVim -.-> ConsumerAppContainer - CtxVim -.-> ConsumerComposer - CtxVim -.-> ConsumerApp - - CtxSettings -.-> ConsumerAppContainer - CtxSettings -.-> ConsumerAppHeader - CtxSettings -.-> ConsumerDialogManager - CtxSettings -.-> ConsumerApp - - CtxApp -.-> ConsumerAppHeader - CtxApp -.-> ConsumerNotifications - - CtxConfig -.-> ConsumerAppHeader - CtxConfig -.-> ConsumerHistoryItem - CtxConfig -.-> ConsumerComposer - CtxConfig -.-> ConsumerDialogManager - - - - CtxUIState -.-> ConsumerApp - CtxUIState -.-> ConsumerMainContent - CtxUIState -.-> ConsumerComposer - CtxUIState -.-> ConsumerDialogManager - - CtxUIActions -.-> ConsumerComposer - CtxUIActions -.-> ConsumerDialogManager - - %% --- Apply Styles --- - %% New Elements (Green) - class B,CtxApp,CtxConfig,CtxUIState,CtxUIActions,ConsumerAppHeader,ConsumerDialogManager,ConsumerComposer,ConsumerMainContent,ConsumerNotifications new - - %% Heavily Changed Elements (Blue) - class A,ConsumerApp,ConsumerAppContainer,ConsumerHistoryItem changed - - %% Mostly Unchanged Elements (Gray) - class CtxSession,CtxVim,CtxSettings unchanged - - %% --- Link Styles --- - %% CtxSession (Red) - linkStyle 0,8,9 stroke:#e57373,stroke-width:2px - %% CtxVim (Orange) - linkStyle 1,10,11,12 stroke:#ffb74d,stroke-width:2px - %% CtxSettings (Yellow) - linkStyle 2,7,13,14,15,16 stroke:#fff176,stroke-width:2px - %% CtxApp (Green) - linkStyle 3,17,18 stroke:#81c784,stroke-width:2px - %% CtxConfig (Blue) - linkStyle 4,19,20,21,22 stroke:#64b5f6,stroke-width:2px - %% CtxUIState (Indigo) - linkStyle 5,23,24,25,26 stroke:#7986cb,stroke-width:2px - %% CtxUIActions (Violet) - linkStyle 6,27,28 stroke:#ba68c8,stroke-width:2px diff --git a/docs/mermaid/render-path.mmd b/docs/mermaid/render-path.mmd deleted file mode 100644 index 5f4c6204..00000000 --- a/docs/mermaid/render-path.mmd +++ /dev/null @@ -1,64 +0,0 @@ -graph TD - %% --- Style Definitions --- - classDef new fill:#98fb98,color:#000 - classDef changed fill:#add8e6,color:#000 - classDef unchanged fill:#f0f0f0,color:#000 - classDef dispatcher fill:#f9e79f,color:#000,stroke:#333,stroke-width:1px - classDef container fill:#f5f5f5,color:#000,stroke:#ccc - - %% --- Component Tree --- - subgraph "Entry Point" - A["gemini.tsx"] - end - - subgraph "State & Logic Wrapper" - B["AppContainer.tsx"] - end - - subgraph "Primary Layout" - C["App.tsx"] - end - - A -.-> B - B -.-> C - - subgraph "UI Containers" - direction LR - C -.-> D["MainContent"] - C -.-> G["Composer"] - C -.-> F["DialogManager"] - C -.-> E["Notifications"] - end - - subgraph "MainContent" - direction TB - D -.-> H["AppHeader"] - D -.-> I["HistoryItemDisplay"]:::dispatcher - D -.-> L["ShowMoreLines"] - end - - subgraph "Composer" - direction TB - G -.-> K_Prompt["InputPrompt"] - G -.-> K_Footer["Footer"] - end - - subgraph "DialogManager" - F -.-> J["Various Dialogs
(Auth, Theme, Settings, etc.)"] - end - - %% --- Apply Styles --- - class B,D,E,F,G,H,J,K_Prompt,L new - class A,C,I changed - class K_Footer unchanged - - %% --- Link Styles --- - %% MainContent Branch (Blue) - linkStyle 2,6,7,8 stroke:#64b5f6,stroke-width:2px - %% Composer Branch (Green) - linkStyle 3,9,10 stroke:#81c784,stroke-width:2px - %% DialogManager Branch (Orange) - linkStyle 4,11 stroke:#ffb74d,stroke-width:2px - %% Notifications Branch (Violet) - linkStyle 5 stroke:#ba68c8,stroke-width:2px - diff --git a/docs/sidebar.json b/docs/sidebar.json deleted file mode 100644 index b4a75052..00000000 --- a/docs/sidebar.json +++ /dev/null @@ -1,68 +0,0 @@ -[ - { - "label": "Overview", - "items": [ - { "label": "Welcome", "slug": "docs" }, - { "label": "Execution and Deployment", "slug": "docs/deployment" }, - { "label": "Architecture Overview", "slug": "docs/architecture" } - ] - }, - { - "label": "CLI", - "items": [ - { "label": "Introduction", "slug": "docs/cli" }, - { "label": "Authentication", "slug": "docs/cli/authentication" }, - { "label": "Commands", "slug": "docs/cli/commands" }, - { "label": "Configuration", "slug": "docs/cli/configuration" }, - { "label": "Checkpointing", "slug": "docs/checkpointing" }, - { "label": "Extensions", "slug": "docs/extension" }, - { "label": "Headless Mode", "slug": "docs/headless" }, - { "label": "IDE Integration", "slug": "docs/ide-integration" }, - { - "label": "IDE Companion Spec", - "slug": "docs/ide-companion-spec" - }, - { "label": "Telemetry", "slug": "docs/telemetry" }, - { "label": "Themes", "slug": "docs/cli/themes" }, - { "label": "Token Caching", "slug": "docs/cli/token-caching" }, - { "label": "Trusted Folders", "slug": "docs/trusted-folders" }, - { "label": "Tutorials", "slug": "docs/cli/tutorials" } - ] - }, - { - "label": "Core", - "items": [ - { "label": "Introduction", "slug": "docs/core" }, - { "label": "Tools API", "slug": "docs/core/tools-api" }, - { "label": "Memory Import Processor", "slug": "docs/core/memport" } - ] - }, - { - "label": "Tools", - "items": [ - { "label": "Overview", "slug": "docs/tools" }, - { "label": "File System", "slug": "docs/tools/file-system" }, - { "label": "Multi-File Read", "slug": "docs/tools/multi-file" }, - { "label": "Shell", "slug": "docs/tools/shell" }, - { "label": "Web Fetch", "slug": "docs/tools/web-fetch" }, - { "label": "Web Search", "slug": "docs/tools/web-search" }, - { "label": "Memory", "slug": "docs/tools/memory" }, - { "label": "MCP Servers", "slug": "docs/tools/mcp-server" }, - { "label": "Sandboxing", "slug": "docs/sandbox" } - ] - }, - { - "label": "Development", - "items": [ - { "label": "NPM", "slug": "docs/npm" }, - { "label": "Releases", "slug": "docs/releases" } - ] - }, - { - "label": "Support", - "items": [ - { "label": "Troubleshooting", "slug": "docs/troubleshooting" }, - { "label": "Terms of Service", "slug": "docs/tos-privacy" } - ] - } -] diff --git a/docs/users/_meta.ts b/docs/users/_meta.ts new file mode 100644 index 00000000..a44167cf --- /dev/null +++ b/docs/users/_meta.ts @@ -0,0 +1,28 @@ +export default { + 'Getting started': { + type: 'separator', + title: 'Getting started', // Title is optional + }, + overview: 'Overview', + quickstart: 'QuickStart', + 'common-workflow': 'Command Workflows', + 'Outside of the terminal': { + type: 'separator', + title: 'Outside of the terminal', // Title is optional + }, + 'integration-vscode': 'Visual Studio Code', + 'integration-zed': 'Zed IDE', + 'integration-github-action': 'Github Actions', + 'Code with Qwen Code': { + type: 'separator', + title: 'Code with Qwen Code', // Title is optional + }, + features: 'Features', + configuration: 'Configuration', + reference: 'Reference', + support: 'Support', + // need refine + 'ide-integration': { + display: 'hidden', + }, +}; diff --git a/docs/users/common-workflow.md b/docs/users/common-workflow.md new file mode 100644 index 00000000..5dde4a97 --- /dev/null +++ b/docs/users/common-workflow.md @@ -0,0 +1,571 @@ +# Common workflows + +> Learn about common workflows with Qwen Code. + +Each task in this document includes clear instructions, example commands, and best practices to help you get the most from Qwen Code. + +## Understand new codebases + +### Get a quick codebase overview + +Suppose you've just joined a new project and need to understand its structure quickly. + +**1. Navigate to the project root directory** + +```bash +cd /path/to/project +``` + +**2. Start Qwen Code** + +```bash +qwen +``` + +**3. Ask for a high-level overview** + +``` +give me an overview of this codebase +``` + +**4. Dive deeper into specific components** + +``` +explain the main architecture patterns used here +``` + +``` +what are the key data models? +``` + +``` +how is authentication handled? +``` + +> [!tip] +> +> - Start with broad questions, then narrow down to specific areas +> - Ask about coding conventions and patterns used in the project +> - Request a glossary of project-specific terms + +### Find relevant code + +Suppose you need to locate code related to a specific feature or functionality. + +**1. Ask Qwen Code to find relevant files** + +``` +find the files that handle user authentication +``` + +**2. Get context on how components interact** + +``` +how do these authentication files work together? +``` + +**3. Understand the execution flow** + +``` +trace the login process from front-end to database +``` + +> [!tip] +> +> - Be specific about what you're looking for +> - Use domain language from the project + +## Fix bugs efficiently + +Suppose you've encountered an error message and need to find and fix its source. + +**1. Share the error with Qwen Code** + +``` +I'm seeing an error when I run npm test +``` + +**2. Ask for fix recommendations** + +``` +suggest a few ways to fix the @ts-ignore in user.ts +``` + +**3. Apply the fix** + +``` +update user.tsto add the null check you suggested +``` + +> [!tip] +> +> - Tell Qwen Code the command to reproduce the issue and get a stack trace +> - Mention any steps to reproduce the error +> - Let Qwen Code know if the error is intermittent or consistent + +## Refactor code + +Suppose you need to update old code to use modern patterns and practices. + +**1. Identify legacy code for refactoring** + +``` +find deprecated API usage in our codebase +``` + +**2. Get refactoring recommendations** + +``` +suggest how to refactor utils.js to use modern JavaScript features +``` + +**3. Apply the changes safely** + +``` +refactor utils.js to use ES 2024 features while maintaining the same behavior +``` + +**4. Verify the refactoring** + +``` +run tests for the refactored code +``` + +> [!tip] +> +> - Ask Qwen Code to explain the benefits of the modern approach +> - Request that changes maintain backward compatibility when needed +> - Do refactoring in small, testable increments + +## Use specialized subagents + +Suppose you want to use specialized AI subagents to handle specific tasks more effectively. + +**1. View available subagents** + +``` +/agents +``` + +This shows all available subagents and lets you create new ones. + +**2. Use subagents automatically** + +Qwen Code automatically delegates appropriate tasks to specialized subagents: + +``` +review my recent code changes for security issues +``` + +``` +run all tests and fix any failures +``` + +**3. Explicitly request specific subagents** + +``` +use the code-reviewer subagent to check the auth module +``` + +``` +have the debugger subagent investigate why users can't log in +``` + +**4. Create custom subagents for your workflow** + +``` +/agents +``` + +Then select "create" and follow the prompts to define: + +- A unique identifier that describes the subagent's purpose (for example, `code-reviewer`, `api-designer`). +- When Qwen Code should use this agent +- Which tools it can access +- A system prompt describing the agent's role and behavior + +> [!tip] +> +> - Create project-specific subagents in `.qwen/agents/` for team sharing +> - Use descriptive `description` fields to enable automatic delegation +> - Limit tool access to what each subagent actually needs +> - Know more about [Sub Agents](/users/features/sub-agents) +> - Know more about [Approval Mode](/users/features/approval-mode) + +## Work with tests + +Suppose you need to add tests for uncovered code. + +**1. Identify untested code** + +``` +find functions in NotificationsService.swift that are not covered by tests +``` + +**2. Generate test scaffolding** + +``` +add tests for the notification service +``` + +**3. Add meaningful test cases** + +``` +add test cases for edge conditions in the notification service +``` + +**4. Run and verify tests** + +``` +run the new tests and fix any failures +``` + +Qwen Code can generate tests that follow your project's existing patterns and conventions. When asking for tests, be specific about what behavior you want to verify. Qwen Code examines your existing test files to match the style, frameworks, and assertion patterns already in use. + +For comprehensive coverage, ask Qwen Code to identify edge cases you might have missed. Qwen Code can analyze your code paths and suggest tests for error conditions, boundary values, and unexpected inputs that are easy to overlook. + +## Create pull requests + +Suppose you need to create a well-documented pull request for your changes. + +**1. Summarize your changes** + +``` +summarize the changes I've made to the authentication module +``` + +**2. Generate a pull request with Qwen Code** + +``` +create a pr +``` + +**3. Review and refine** + +``` +enhance the PR description with more context about the security improvements +``` + +**4. Add testing details** + +``` +add information about how these changes were tested +``` + +> [!tip] +> +> - Ask Qwen Code directly to make a PR for you +> - Review Qwen Code's generated PR before submitting +> - Ask Qwen Code to highlight potential risks or considerations + +## Handle documentation + +Suppose you need to add or update documentation for your code. + +**1. Identify undocumented code** + +``` +find functions without proper JSDoc comments in the auth module +``` + +**2. Generate documentation** + +``` +add JSDoc comments to the undocumented functions in auth.js +``` + +**3. Review and enhance** + +``` +improve the generated documentation with more context and examples +``` + +**4. Verify documentation** + +``` +check if the documentation follows our project standards +``` + +> [!tip] +> +> - Specify the documentation style you want (JSDoc, docstrings, etc.) +> - Ask for examples in the documentation +> - Request documentation for public APIs, interfaces, and complex logic + +## Reference files and directories + +Use `@` to quickly include files or directories without waiting for Qwen Code to read them. + +**1. Reference a single file** + +``` +Explain the logic in @src/utils/auth.js +``` + +This includes the full content of the file in the conversation. + +**2. Reference a directory** + +``` +What's the structure of @src/components? +``` + +This provides a directory listing with file information. + +**3. Reference MCP resources** + +``` +Show me the data from @github: repos/owner/repo/issues +``` + +This fetches data from connected MCP servers using the format @server: resource. See [MCP](/users/features/mcp) for details. + +> [!tip] +> +> - File paths can be relative or absolute +> - @ file references add `QWEN.md` in the file's directory and parent directories to context +> - Directory references show file listings, not contents +> - You can reference multiple files in a single message (for example, "`@file 1.js` and `@file 2.js`") + +## Resume previous conversations + +Suppose you've been working on a task with Qwen Code and need to continue where you left off in a later session. + +Qwen Code provides two options for resuming previous conversations: + +- `--continue` to automatically continue the most recent conversation +- `--resume` to display a conversation picker + +**1. Continue the most recent conversation** + +```bash +qwen --continue +``` + +This immediately resumes your most recent conversation without any prompts. + +**2. Continue in non-interactive mode** + +```bash +qwen --continue --p "Continue with my task" +``` + +Use `--print` with `--continue` to resume the most recent conversation in non-interactive mode, perfect for scripts or automation. + +**3. Show conversation picker** + +```bash +qwen --resume +``` + +This displays an interactive conversation selector with a clean list view showing: + +- Session summary (or initial prompt) +- Metadata: time elapsed, message count, and git branch + +Use arrow keys to navigate and press Enter to select a conversation. Press Esc to exit. + +> [!tip] +> +> - Conversation history is stored locally on your machine +> - Use `--continue` for quick access to your most recent conversation +> - Use `--resume` when you need to select a specific past conversation +> - When resuming, you'll see the entire conversation history before continuing +> - The resumed conversation starts with the same model and configuration as the original +> +> **How it works**: +> +> 1. **Conversation Storage**: All conversations are automatically saved locally with their full message history +> 2. **Message Deserialization**: When resuming, the entire message history is restored to maintain context +> 3. **Tool State**: Tool usage and results from the previous conversation are preserved +> 4. **Context Restoration**: The conversation resumes with all previous context intact +> +> **Examples**: +> +> ```bash +> # Continue most recent conversation +> qwen --continue +> +> # Continue most recent conversation with a specific prompt +> qwen --continue --p "Show me our progress" +> +> # Show conversation picker +> qwen --resume +> +> # Continue most recent conversation in non-interactive mode +> qwen --continue --p "Run the tests again" +> ``` + +## Run parallel Qwen Code sessions with Git worktrees + +Suppose you need to work on multiple tasks simultaneously with complete code isolation between Qwen Code instances. + +**1. Understand Git worktrees** + +Git worktrees allow you to check out multiple branches from the same repository into separate directories. Each worktree has its own working directory with isolated files, while sharing the same Git history. Learn more in the [official Git worktree documentation](https://git-scm.com/docs/git-worktree). + +**2. Create a new worktree** + +```bash +# Create a new worktree with a new branch +git worktree add ../project-feature-a -b feature-a + +# Or create a worktree with an existing branch +git worktree add ../project-bugfix bugfix-123 +``` + +This creates a new directory with a separate working copy of your repository. + +**3. Run Qwen Code in each worktree** + +```bash +# Navigate to your worktree +cd ../project-feature-a + +# Run Qwen Code in this isolated environment +qwen +``` + +**4. Run Qwen Code in another worktree** + +```bash +cd ../project-bugfix +qwen +``` + +**5. Manage your worktrees** + +```bash +# List all worktrees +git worktree list + +# Remove a worktree when done +git worktree remove ../project-feature-a +``` + +> [!tip] +> +> - Each worktree has its own independent file state, making it perfect for parallel Qwen Code sessions +> - Changes made in one worktree won't affect others, preventing Qwen Code instances from interfering with each other +> - All worktrees share the same Git history and remote connections +> - For long-running tasks, you can have Qwen Code working in one worktree while you continue development in another +> - Use descriptive directory names to easily identify which task each worktree is for +> - Remember to initialize your development environment in each new worktree according to your project's setup. Depending on your stack, this might include: +> - JavaScript projects: Running dependency installation (`npm install`, `yarn`) +> - Python projects: Setting up virtual environments or installing with package managers +> - Other languages: Following your project's standard setup process + +## Use Qwen Code as a unix-style utility + +### Add Qwen Code to your verification process + +Suppose you want to use Qwen Code as a linter or code reviewer. + +**Add Qwen Code to your build script:** + +```json +// package.json +{ + ... + "scripts": { + ... + "lint:Qwen Code": "qwen -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'" + } +} +``` + +> [!tip] +> +> - Use Qwen Code for automated code review in your CI/CD pipeline +> - Customize the prompt to check for specific issues relevant to your project +> - Consider creating multiple scripts for different types of verification + +### Pipe in, pipe out + +Suppose you want to pipe data into Qwen Code, and get back data in a structured format. + +**Pipe data through Qwen Code:** + +```bash +cat build-error.txt | qwen -p 'concisely explain the root cause of this build error' > output.txt +``` + +> [!tip] +> +> - Use pipes to integrate Qwen-Code into existing shell scripts +> - Combine with other Unix tools for powerful workflows +> - Consider using --output-format for structured output + +### Control output format + +Suppose you need Qwen Code's output in a specific format, especially when integrating Qwen Code into scripts or other tools. + +**1. Use text format (default)** + +```bash +cat data.txt | qwen -p 'summarize this data' --output-format text > summary.txt +``` + +This outputs just Qwen Code's plain text response (default behavior). + +**2. Use JSON format** + +```bash +cat code.py | qwen -p 'analyze this code for bugs' --output-format json > analysis.json +``` + +This outputs a JSON array of messages with metadata including cost and duration. + +**3. Use streaming JSON format** + +```bash +cat log.txt | qwen -p 'parse this log file for errors' --output-format stream-json +``` + +This outputs a series of JSON objects in real-time as Qwen Code processes the request. Each message is a valid JSON object, but the entire output is not valid JSON if concatenated. + +> [!tip] +> +> - Use `--output-format text` for simple integrations where you just need Qwen Code's response +> - Use `--output-format json` when you need the full conversation log +> - Use `--output-format stream-json` for real-time output of each conversation turn + +## Ask Qwen Code about its capabilities + +Qwen Code has built-in access to its documentation and can answer questions about its own features and limitations. + +### Example questions + +``` +can Qwen Code create pull requests? +``` + +``` +how does Qwen Code handle permissions? +``` + +``` +what slash commands are available? +``` + +``` +how do I use MCP with Qwen Code? +``` + +``` +how do I configure Qwen Code for Amazon Bedrock? +``` + +``` +what are the limitations of Qwen Code? +``` + +> [!note] +> +> Qwen Code provides documentation-based answers to these questions. For executable examples and hands-on demonstrations, refer to the specific workflow sections above. + +> [!tip] +> +> - Qwen Code always has access to the latest Qwen Code documentation, regardless of the version you're using +> - Ask specific questions to get detailed answers +> - Qwen Code can explain complex features like MCP integration, enterprise configurations, and advanced workflows diff --git a/docs/users/configuration/_meta.ts b/docs/users/configuration/_meta.ts new file mode 100644 index 00000000..8899eb91 --- /dev/null +++ b/docs/users/configuration/_meta.ts @@ -0,0 +1,10 @@ +export default { + settings: 'Settings', + auth: 'Authentication', + memory: { + display: 'hidden', + }, + 'qwen-ignore': 'Ignoring Files', + 'trusted-folders': 'Trusted Folders', + themes: 'Themes', +}; diff --git a/docs/users/configuration/auth.md b/docs/users/configuration/auth.md new file mode 100644 index 00000000..82bc66b2 --- /dev/null +++ b/docs/users/configuration/auth.md @@ -0,0 +1,119 @@ +# Authentication + +Qwen Code supports two authentication methods. Pick the one that matches how you want to run the CLI: + +- **Qwen OAuth (recommended)**: sign in with your `qwen.ai` account in a browser. +- **OpenAI-compatible API**: use an API key (OpenAI or any OpenAI-compatible provider / endpoint). + +## Option 1: Qwen OAuth (recommended & free) 👍 + +Use this if you want the simplest setup and you’re using Qwen models. + +- **How it works**: on first start, Qwen Code opens a browser login page. After you finish, credentials are cached locally so you usually won’t need to log in again. +- **Requirements**: a `qwen.ai` account + internet access (at least for the first login). +- **Benefits**: no API key management, automatic credential refresh. +- **Cost & quota**: free, with a quota of **60 requests/minute** and **2,000 requests/day**. + +Start the CLI and follow the browser flow: + +```bash +qwen +``` + +## Option 2: OpenAI-compatible API (API key) + +Use this if you want to use OpenAI models or any provider that exposes an OpenAI-compatible API (e.g. OpenAI, Azure OpenAI, OpenRouter, ModelScope, Alibaba Cloud Bailian, or a self-hosted compatible endpoint). + +### Quick start (interactive, recommended for local use) + +When you choose the OpenAI-compatible option in the CLI, it will prompt you for: + +- **API key** +- **Base URL** (default: `https://api.openai.com/v1`) +- **Model** (default: `gpt-4o`) + +> **Note:** the CLI may display the key in plain text for verification. Make sure your terminal is not being recorded or shared. + +### Configure via command-line arguments + +```bash +# API key only +qwen-code --openai-api-key "your-api-key-here" + +# Custom base URL (OpenAI-compatible endpoint) +qwen-code --openai-api-key "your-api-key-here" --openai-base-url "https://your-endpoint.com/v1" + +# Custom model +qwen-code --openai-api-key "your-api-key-here" --model "gpt-4o-mini" +``` + +### Configure via environment variables + +You can set these in your shell profile, CI, or an `.env` file: + +```bash +export OPENAI_API_KEY="your-api-key-here" +export OPENAI_BASE_URL="https://api.openai.com/v1" # optional +export OPENAI_MODEL="gpt-4o" # optional +``` + +#### Persisting env vars with `.env` / `.qwen/.env` + +Qwen Code will auto-load environment variables from the **first** `.env` file it finds (variables are **not merged** across multiple files). + +Search order: + +1. From the **current directory**, walking upward toward `/`: + 1. `.qwen/.env` + 2. `.env` +2. If nothing is found, it falls back to your **home directory**: + - `~/.qwen/.env` + - `~/.env` + +`.qwen/.env` is recommended to keep Qwen Code variables isolated from other tools. Some variables (like `DEBUG` and `DEBUG_MODE`) are excluded from project `.env` files to avoid interfering with qwen-code behavior. + +Examples: + +```bash +# Project-specific settings (recommended) +mkdir -p .qwen +cat >> .qwen/.env <<'EOF' +OPENAI_API_KEY="your-api-key" +OPENAI_BASE_URL="https://api-inference.modelscope.cn/v1" +OPENAI_MODEL="Qwen/Qwen3-Coder-480B-A35B-Instruct" +EOF +``` + +```bash +# User-wide settings (available everywhere) +mkdir -p ~/.qwen +cat >> ~/.qwen/.env <<'EOF' +OPENAI_API_KEY="your-api-key" +OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" +OPENAI_MODEL="qwen3-coder-plus" +EOF +``` + +## Switch authentication method (without restarting) + +In the Qwen Code UI, run: + +```bash +/auth +``` + +## Non-interactive / headless environments (CI, SSH, containers) + +In a non-interactive terminal you typically **cannot** complete the OAuth browser login flow. +Use the OpenAI-compatible API method via environment variables: + +- Set at least `OPENAI_API_KEY`. +- Optionally set `OPENAI_BASE_URL` and `OPENAI_MODEL`. + +If none of these are set in a non-interactive session, Qwen Code will exit with an error. + +## Security notes + +- Don’t commit API keys to version control. +- Prefer `.qwen/.env` for project-local secrets (and keep it out of git). +- Treat your terminal output as sensitive if it prints credentials for verification. diff --git a/docs/users/configuration/memory.md b/docs/users/configuration/memory.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/cli/qwen-ignore.md b/docs/users/configuration/qwen-ignore.md similarity index 76% rename from docs/cli/qwen-ignore.md rename to docs/users/configuration/qwen-ignore.md index 1b870910..25087657 100644 --- a/docs/cli/qwen-ignore.md +++ b/docs/users/configuration/qwen-ignore.md @@ -6,7 +6,7 @@ Qwen Code includes the ability to automatically ignore files, similar to `.gitig ## How it works -When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](./tools/multi-file.md) command, any paths in your `.qwenignore` file will be automatically excluded. +When you add a path to your `.qwenignore` file, tools that respect this file will exclude matching files and directories from their operations. For example, when you use the [`read_many_files`](/developers/tools/multi-file) command, any paths in your `.qwenignore` file will be automatically excluded. For the most part, `.qwenignore` follows the conventions of `.gitignore` files: @@ -20,14 +20,10 @@ You can update your `.qwenignore` file at any time. To apply the changes, you mu ## How to use `.qwenignore` -To enable `.qwenignore`: - -1. Create a file named `.qwenignore` in the root of your project directory. - -To add a file or directory to `.qwenignore`: - -1. Open your `.qwenignore` file. -2. Add the path or file you want to ignore, for example: `/archive/` or `apikeys.txt`. +| Step | Description | +| ---------------------- | ------------------------------------------------------------ | +| **Enable .qwenignore** | Create a file named `.qwenignore` in your project root directory | +| **Add ignore rules** | Open `.qwenignore` file and add paths to ignore, example: `/archive/` or `apikeys.txt` | ### `.qwenignore` examples diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md new file mode 100644 index 00000000..a62ef2fe --- /dev/null +++ b/docs/users/configuration/settings.md @@ -0,0 +1,506 @@ +# Qwen Code Configuration + +> [!tip] +> +> **Authentication / API keys:** Authentication (Qwen OAuth vs OpenAI-compatible API) and auth-related environment variables (like `OPENAI_API_KEY`) are documented in **[Authentication](/users/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](/users/features/sandbox) (e.g. `.qwen/sandbox-macos-custom.sb`, `.qwen/sandbox.Dockerfile`). + +### 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.disableAutoUpdate` | boolean | Disable automatic updates. | `false` | +| `general.disableUpdateNag` | boolean | Disable update notification prompts. | `false` | +| `general.checkpointing.enabled` | boolean | Enable session checkpointing for recovery. | `false` | + +#### 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](/users/configuration/themes) for available options. | `undefined` | +| `ui.customThemes` | object | Custom theme definitions. | `{}` | +| `ui.hideWindowTitle` | boolean | Hide the window title bar. | `false` | +| `ui.hideTips` | boolean | Hide helpful tips in the UI. | `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` | +| `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. This feature integrates with the `/summary` command and quit confirmation dialog. | `true` | +| `ui.accessibility.disableLoadingPhrases` | boolean | Disable loading phrases for accessibility. | `false` | +| `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. | `[]` | + +#### 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.summarizeToolOutput` | object | Enables or disables the summarization of tool output. You can specify the token budget for the summarization using the `tokenBudget` setting. Note: Currently only the `run_shell_command` tool is supported. For example `{"run_shell_command": {"tokenBudget": 2000}}` | `undefined` | +| `model.generationConfig` | object | Advanced overrides passed to the underlying content generator. Supports request controls such as `timeout`, `maxRetries`, and `disableCacheControl`, 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:** + +``` +{ + "model": { + "generationConfig": { + "timeout": 60000, + "disableCacheControl": false, + "samplingParams": { + "temperature": 0.2, + "top_p": 0.8, + "max_tokens": 1024 + } + } + } +} +``` + +**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` + +#### 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.discoveryMaxDirs` | number | Maximum number of directories to search for memory. | `200` | +| `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.disableFuzzySearch` | boolean | When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files. | `false` | + +#### 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 `disableFuzzySearch` to `true` 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.shell.enableInteractiveShell` | boolean | Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. | `false` | | +| `tools.core` | array of strings | This can be used to restrict the set of built-in tools with an allowlist. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"tools.core": ["run_shell_command(ls -l)"]` will only allow the `ls -l` command to be executed. | `undefined` | | +| `tools.exclude` | array of strings | Tool names to exclude from discovery. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"tools.exclude": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. **Security Note:** Command-specific restrictions in `tools.exclude` for `run_shell_command` are based on simple string matching and can be easily bypassed. This feature is **not a security mechanism** and should not be relied upon to safely execute untrusted code. It is recommended to use `tools.core` to explicitly select commands that can be executed. | `undefined` | | +| `tools.allowed` | array of strings | A list of tool names that will bypass the confirmation dialog. This is useful for tools that you trust and use frequently. For example, `["run_shell_command(git)", "run_shell_command(npm test)"]` will skip the confirmation dialog to run any `git` and `npm test` commands. | `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.enableToolOutputTruncation` | boolean | Enable truncation of large tool outputs. | `true` | Requires restart: Yes | +| `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 | +| `tools.autoAccept` | boolean | Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe. | `false` | | + +#### 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. + +#### 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", + "hideBanner": true, + "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", + "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", + "summarizeToolOutput": { + "run_shell_command": { + "tokenBudget": 100 + } + } + }, + "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](/users/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 | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GEMINI_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. | +| `GEMINI_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. | +| `GEMINI_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. | +| `GEMINI_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. | +| `GEMINI_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. | +| `GEMINI_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. | +| `GEMINI_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. | +| `GEMINI_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. | +| `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. | +| `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"` | + +## Command-Line Arguments + +Arguments passed directly when running the CLI can override other configurations for that specific session. + +### 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"` | +| `--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](/users/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](/users/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](/users/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](/users/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)"` | +| `--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](/users/features/checkpointing). | | | +| `--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 sophisticated 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. + 3. **Sub-directory Context Files (Contextual/Local):** + - Location: The CLI also scans for the configured context file in subdirectories _below_ the current working directory (respecting common ignore patterns like `node_modules`, `.git`, etc.). The breadth of this search is limited to 200 directories by default, but can be configured with the `context.discoveryMaxDirs` setting in your `settings.json` file. + - Scope: Allows for highly specific instructions relevant to a particular component, module, or subsection of your project. +- **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](/users/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](/users/reference/cli-reference) 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](/users/features/sandbox) is disabled by default, but you can enable it in a few ways: + +- Using `--sandbox` or `-s` flag. +- Setting `GEMINI_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/docs/cli/themes.md b/docs/users/configuration/themes.md similarity index 66% rename from docs/cli/themes.md rename to docs/users/configuration/themes.md index 3b262bc9..d17498ea 100644 --- a/docs/cli/themes.md +++ b/docs/users/configuration/themes.md @@ -140,7 +140,9 @@ The theme file must be a valid JSON file that follows the same structure as a cu ### Example Custom Theme -Custom theme example + + + ### Using Your Custom Theme @@ -148,56 +150,15 @@ The theme file must be a valid JSON file that follows the same structure as a cu - Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui` object in your `settings.json`. - Custom themes can be set at the user, project, or system level, and follow the same [configuration precedence](./configuration.md) as other settings. ---- -## Dark Themes -### ANSI +## Themes Preview -ANSI theme - -### Atom OneDark - -Atom One theme - -### Ayu - -Ayu theme - -### Default - -Default theme - -### Dracula - -Dracula theme - -### GitHub - -GitHub theme - -## Light Themes - -### ANSI Light - -ANSI Light theme - -### Ayu Light - -Ayu Light theme - -### Default Light - -Default Light theme - -### GitHub Light - -GitHub Light theme - -### Google Code - -Google Code theme - -### Xcode - -Xcode Light theme +| Dark Theme | Preview | Light Theme | Preview | +| :-: | :-: | :-: | :-: | +| ANSI | | ANSI Light | | +| Atom OneDark | | Ayu Light |  | +| Ayu |  | Default Light |  | +| Default | | GitHub Light |  | +| Dracula | | Google Code |  | +| GitHub |  | Xcode |  | diff --git a/docs/cli/trusted-folders.md b/docs/users/configuration/trusted-folders.md similarity index 90% rename from docs/cli/trusted-folders.md rename to docs/users/configuration/trusted-folders.md index 6fe3486b..afe955ef 100644 --- a/docs/cli/trusted-folders.md +++ b/docs/users/configuration/trusted-folders.md @@ -22,8 +22,8 @@ Add the following to your user `settings.json` file: Once the feature is enabled, the first time you run the Qwen Code from a folder, a dialog will automatically appear, prompting you to make a choice: -- **Trust folder**: Grants full trust to the current folder (e.g., `my-project`). -- **Trust parent folder**: Grants trust to the parent directory (e.g., `safe-projects`), which automatically trusts all of its subdirectories as well. This is useful if you keep all your safe projects in one place. +- **Trust folder**: Grants full trust to the current folder (e.g. `my-project`). +- **Trust parent folder**: Grants trust to the parent directory (e.g. `safe-projects`), which automatically trusts all of its subdirectories as well. This is useful if you keep all your safe projects in one place. - **Don't trust**: Marks the folder as untrusted. The CLI will operate in a restricted "safe mode." Your choice is saved in a central file (`~/.qwen/trustedFolders.json`), so you will only be asked once per folder. @@ -56,6 +56,6 @@ If you need to change a decision or see all your settings, you have a couple of For advanced users, it's helpful to know the exact order of operations for how trust is determined: -1. **IDE Trust Signal**: If you are using the [IDE Integration](./ide-integration.md), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority. +1. **IDE Trust Signal**: If you are using the [IDE Integration](/users/ide-integration/ide-integration), the CLI first asks the IDE if the workspace is trusted. The IDE's response takes highest priority. 2. **Local Trust File**: If the IDE is not connected, the CLI checks the central `~/.qwen/trustedFolders.json` file. diff --git a/docs/users/features/_meta.ts b/docs/users/features/_meta.ts new file mode 100644 index 00000000..5509cc74 --- /dev/null +++ b/docs/users/features/_meta.ts @@ -0,0 +1,12 @@ +export default { + commands: 'Commands', + 'sub-agents': 'SubAgents', + headless: 'Headless Mode', + checkpointing: { + display: 'hidden', + }, + 'approval-mode': 'Approval Mode', + mcp: 'MCP', + 'token-caching': 'Token Caching', + sandbox: 'Sandboxing', +}; diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md new file mode 100644 index 00000000..0749140e --- /dev/null +++ b/docs/users/features/approval-mode.md @@ -0,0 +1,261 @@ +Qwen Code offers three distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level. + +## Permission Modes Comparison + +| Mode | File Editing | Shell Commands | Best For | Risk Level | +| -------------- | --------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | ---------- | +| **Plan**​ | ❌ Read-only analysis only | ❌ Not executed | • Code exploration
• Planning complex changes
• Safe code review | Lowest | +| **Default**​ | ✅ Manual approval required | ✅ Manual approval required | • New/unfamiliar codebases
• Critical systems
• Team collaboration
• Learning and teaching | Low | +| **Auto-Edit**​ | ✅ Auto-approved | ❌ Manual approval required | • Daily development tasks
• Refactoring and code improvements
• Safe automation | Medium | +| **YOLO**​ | ✅ Auto-approved | ✅ Auto-approved | • Trusted personal projects
• Automated scripts/CI/CD
• Batch processing tasks | Highest | + +### Quick Reference Guide + +- **Start in Plan Mode**: Great for understanding before making changes +- **Work in Default Mode**: The balanced choice for most development work +- **Switch to Auto-Edit**: When you're making lots of safe code changes +- **Use YOLO sparingly**: Only for trusted automation in controlled environments + +> [!tip] +> +> You can quickly cycle through modes during a session using **Shift+Tab**. The terminal status bar shows your current mode, so you always know what permissions Qwen Code has. + +## 1. Use Plan Mode for safe code analysis + +Plan Mode instructs Qwen Code to create a plan by analyzing the codebase with **read-only** operations, perfect for exploring codebases, planning complex changes, or reviewing code safely. + +### When to use Plan Mode + +- **Multi-step implementation**: When your feature requires making edits to many files +- **Code exploration**: When you want to research the codebase thoroughly before changing anything +- **Interactive development**: When you want to iterate on the direction with Qwen Code + +### How to use Plan Mode + +**Turn on Plan Mode during a session** + +You can switch into Plan Mode during a session using **Shift+Tab** to cycle through permission modes. + +If you are in Normal Mode, **Shift+Tab** first switches into `auto-edits` Mode, indicated by `⏵⏵ accept edits on` at the bottom of the terminal. A subsequent **Shift+Tab** will switch into Plan Mode, indicated by `⏸ plan mode`. + +**Start a new session in Plan Mode** + +To start a new session in Plan Mode, use the `/approval-mode` then select `plan` + +```bash +/approval-mode +``` + +**Run "headless" queries in Plan Mode** + +You can also run a query in Plan Mode directly with `-p` or `prompt`: + +```bash +qwen --prompt "What is machine learning?" +``` + +### Example: Planning a complex refactor + +```bash +/approval-mode plan +``` + +``` +I need to refactor our authentication system to use OAuth2. Create a detailed migration plan. +``` + +Qwen Code analyzes the current implementation and create a comprehensive plan. Refine with follow-ups: + +``` +What about backward compatibility? +How should we handle database migration? +``` + +### Configure Plan Mode as default + +```json +// .qwen/settings.json +{ + "permissions": { + "defaultMode": "plan" + } +} +``` + +## 2. Use Default Mode for Controlled Interaction + +Default Mode is the standard way to work with Qwen Code. In this mode, you maintain full control over all potentially risky operations - Qwen Code will ask for your approval before making any file changes or executing shell commands. + +### When to use Default Mode + +- **New to a codebase**: When you're exploring an unfamiliar project and want to be extra cautious +- **Critical systems**: When working on production code, infrastructure, or sensitive data +- **Learning and teaching**: When you want to understand each step Qwen Code is taking +- **Team collaboration**: When multiple people are working on the same codebase +- **Complex operations**: When the changes involve multiple files or complex logic + +### How to use Default Mode + +**Turn on Default Mode during a session** + +You can switch into Default Mode during a session using **Shift+Tab**​ to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab**​ will eventually cycle back to Default Mode, indicated by the absence of any mode indicator at the bottom of the terminal. + +**Start a new session in Default Mode** + +Default Mode is the initial mode when you start Qwen Code. If you've changed modes and want to return to Default Mode, use: + +``` +/approval-mode default +``` + +**Run "headless" queries in Default Mode** + +When running headless commands, Default Mode is the default behavior. You can explicitly specify it with: + +``` +qwen --prompt "Analyze this code for potential bugs" +``` + +### Example: Safely implementing a feature + +``` +/approval-mode default +``` + +``` +I need to add user profile pictures to our application. The pictures should be stored in an S3 bucket and the URLs saved in the database. +``` + +Qwen Code will analyze your codebase and propose a plan. It will then ask for approval before: + +1. Creating new files (controllers, models, migrations) +2. Modifying existing files (adding new columns, updating APIs) +3. Running any shell commands (database migrations, dependency installation) + +You can review each proposed change and approve or reject it individually. + +### Configure Default Mode as default + +```bash +// .qwen/settings.json +{ + "permissions": { +"defaultMode": "default" + } +} +``` + +## 3. Auto Edits Mode + +Auto-Edit Mode instructs Qwen Code to automatically approve file edits while requiring manual approval for shell commands, ideal for accelerating development workflows while maintaining system safety. + +### When to use Auto-Accept Edits Mode + +- **Daily development**: Ideal for most coding tasks +- **Safe automation**: Allows AI to modify code while preventing accidental execution of dangerous commands +- **Team collaboration**: Use in shared projects to avoid unintended impacts on others + +### How to switch to this mode + +``` +# Switch via command +/approval-mode auto-edit + +# Or use keyboard shortcut +Shift+Tab # Switch from other modes +``` + +### Workflow Example + +1. You ask Qwen Code to refactor a function +2. AI analyzes the code and proposes changes +3. **Automatically**​ applies all file changes without confirmation +4. If tests need to be run, it will **request approval**​ to execute `npm test` + +## 4. YOLO Mode - Full Automation + +YOLO Mode grants Qwen Code the highest permissions, automatically approving all tool calls including file editing and shell commands. + +### When to use YOLO Mode + +- **Automated scripts**: Running predefined automated tasks +- **CI/CD pipelines**: Automated execution in controlled environments +- **Personal projects**: Rapid iteration in fully trusted environments +- **Batch processing**: Tasks requiring multi-step command chains + +> [!warning] +> +> **Use YOLO Mode with caution**: AI can execute any command with your terminal permissions. Ensure: +> +> 1. You trust the current codebase +> 2. You understand all actions AI will perform +> 3. Important files are backed up or committed to version control + +### How to enable YOLO Mode + +``` +# Temporarily enable (current session only) +/approval-mode yolo + +# Set as project default +/approval-mode yolo --project + +# Set as user global default +/approval-mode yolo --user +``` + +### Configuration Example + +```bash +// .qwen/settings.json +{ + "permissions": { +"defaultMode": "yolo", +"confirmShellCommands": false, +"confirmFileEdits": false + } +} +``` + +### Automated Workflow Example + +```bash +# Fully automated refactoring task +qwen --prompt "Run the test suite, fix all failing tests, then commit changes" + +# Without human intervention, AI will: +# 1. Run test commands (auto-approved) +# 2. Fix failed test cases (auto-edit files) +# 3. Execute git commit (auto-approved) +``` + +## Mode Switching & Configuration + +### Keyboard Shortcut Switching + +During a Qwen Code session, use **Shift+Tab**​ to quickly cycle through the three modes: + +``` +Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode +``` + +### Persistent Configuration + +``` +// Project-level: ./.qwen/settings.json +// User-level: ~/.qwen/settings.json +{ + "permissions": { +"defaultMode": "auto-edit", // or "plan" or "yolo" +"confirmShellCommands": true, +"confirmFileEdits": true + } +} +``` + +### Mode Usage Recommendations + +1. **New to codebase**: Start with **Plan Mode**​ for safe exploration +2. **Daily development tasks**: Use **Auto-Accept Edits**​ (default mode), efficient and safe +3. **Automated scripts**: Use **YOLO Mode**​ in controlled environments for full automation +4. **Complex refactoring**: Use **Plan Mode**​ first for detailed planning, then switch to appropriate mode for execution diff --git a/docs/features/checkpointing.md b/docs/users/features/checkpointing.md similarity index 100% rename from docs/features/checkpointing.md rename to docs/users/features/checkpointing.md diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md new file mode 100644 index 00000000..92833b4e --- /dev/null +++ b/docs/users/features/commands.md @@ -0,0 +1,264 @@ +# Commands + +This document details all commands supported by Qwen Code, helping you efficiently manage sessions, customize the interface, and control its behavior. + +Qwen Code commands are triggered through specific prefixes and fall into three categories: + +| Prefix Type | Function Description | Typical Use Case | +| -------------------------- | --------------------------------------------------- | ---------------------------------------------------------------- | +| Slash Commands (`/`) | Meta-level control of Qwen Code itself | Managing sessions, modifying settings, getting help | +| At Commands (`@`) | Quickly inject local file content into conversation | Allowing AI to analyze specified files or code under directories | +| Exclamation Commands (`!`) | Direct interaction with system Shell | Executing system commands like `git status`, `ls`, etc. | + +## 1. Slash Commands (`/`) + +Slash commands are used to manage Qwen Code sessions, interface, and basic behavior. + +### 1.1 Session and Project Management + +These commands help you save, restore, and summarize work progress. + +| Command | Description | Usage Examples | +| ----------- | --------------------------------------------------------- | ------------------------------------ | +| `/summary` | Generate project summary based on conversation history | `/summary` | +| `/compress` | Replace chat history with summary to save Tokens | `/compress` | +| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore ` | +| `/init` | Analyze current directory and create initial context file | `/init` | + +### 1.2 Interface and Workspace Control + +Commands for adjusting interface appearance and work environment. + +| Command | Description | Usage Examples | +| ------------ | ---------------------------------------- | ----------------------------- | +| `/clear` | Clear terminal screen content | `/clear` (shortcut: `Ctrl+L`) | +| `/theme` | Change Qwen Code visual theme | `/theme` | +| `/vim` | Turn input area Vim editing mode on/off | `/vim` | +| `/directory` | Manage multi-directory support workspace | `/dir add ./src,./tests` | +| `/editor` | Open dialog to select supported editor | `/editor` | + +### 1.3 Language Settings + +Commands specifically for controlling interface and output language. + +| Command | Description | Usage Examples | +| --------------------- | -------------------------------- | -------------------------- | +| `/language` | View or change language settings | `/language` | +| → `ui [language]` | Set UI interface language | `/language ui zh-CN` | +| → `output [language]` | Set LLM output language | `/language output Chinese` | + +- Available UI languages: `zh-CN` (Simplified Chinese), `en-US` (English) +- Output language examples: `Chinese`, `English`, `Japanese`, etc. + +### 1.4 Tool and Model Management + +Commands for managing AI tools and models. + +| Command | Description | Usage Examples | +| ---------------- | --------------------------------------------- | --------------------------------------------- | +| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` | +| `/tools` | Display currently available tool list | `/tools`, `/tools desc` | +| `/approval-mode` | Change approval mode for tool usage | `/approval-mode --project` | +| →`plan` | Analysis only, no execution | Secure review | +| →`default` | Require approval for edits | Daily use | +| →`auto-edit` | Automatically approve edits | Trusted environment | +| →`yolo` | Automatically approve all | Quick prototyping | +| `/model` | Switch model used in current session | `/model` | +| `/extensions` | List all active extensions in current session | `/extensions` | +| `/memory` | Manage AI's instruction context | `/memory add Important Info` | + +### 1.5 Information, Settings, and Help + +Commands for obtaining information and performing system settings. + +| Command | Description | Usage Examples | +| --------------- | ----------------------------------------------- | ------------------------------------------------ | +| `/help` | Display help information for available commands | `/help` or `/?` | +| `/about` | Display version information | `/about` | +| `/stats` | Display detailed statistics for current session | `/stats` | +| `/settings` | Open settings editor | `/settings` | +| `/auth` | Change authentication method | `/auth` | +| `/bug` | Submit issue about Qwen Code | `/bug Button click unresponsive` | +| `/copy` | Copy last output content to clipboard | `/copy` | +| `/quit-confirm` | Show confirmation dialog before quitting | `/quit-confirm` (shortcut: press `Ctrl+C` twice) | +| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` | + +### 1.6 Common Shortcuts + +| Shortcut | Function | Note | +| ------------------ | ----------------------- | ---------------------- | +| `Ctrl/cmd+L` | Clear screen | Equivalent to `/clear` | +| `Ctrl/cmd+T` | Toggle tool description | MCP tool management | +| `Ctrl/cmd+C`×2 | Exit confirmation | Secure exit mechanism | +| `Ctrl/cmd+Z` | Undo input | Text editing | +| `Ctrl/cmd+Shift+Z` | Redo input | Text editing | + +## 2. @ Commands (Introducing Files) + +@ commands are used to quickly add local file or directory content to the conversation. + +| Command Format | Description | Examples | +| ------------------- | -------------------------------------------- | ------------------------------------------------ | +| `@` | Inject content of specified file | `@src/main.py Please explain this code` | +| `@` | Recursively read all text files in directory | `@docs/ Summarize content of this document` | +| Standalone `@` | Used when discussing `@` symbol itself | `@ What is this symbol used for in programming?` | + +Note: Spaces in paths need to be escaped with backslash (e.g., `@My\ Documents/file.txt`) + +## 3. Exclamation Commands (`!`) - Shell Command Execution + +Exclamation commands allow you to execute system commands directly within Qwen Code. + +| Command Format | Description | Examples | +| ------------------ | ------------------------------------------------------------------ | -------------------------------------- | +| `!` | Execute command in sub-Shell | `!ls -la`, `!git status` | +| Standalone `!` | Switch Shell mode, any input is executed directly as Shell command | `!`(enter) → Input command → `!`(exit) | + +Environment Variables: Commands executed via `!` will set the `QWEN_CODE=1` environment variable. + +## 4. Custom Commands + +Save frequently used prompts as shortcut commands to improve work efficiency and ensure consistency. + +### Quick Overview + +| Function | Description | Advantages | Priority | Applicable Scenarios | +| ---------------- | ------------------------------------------ | -------------------------------------- | -------- | ---------------------------------------------------- | +| Namespace | Subdirectory creates colon-named commands | Better command organization | | | +| Global Commands | `~/.qwen/commands/` | Available in all projects | Low | Personal frequently used commands, cross-project use | +| Project Commands | `/.qwen/commands/` | Project-specific, version-controllable | High | Team sharing, project-specific commands | + +Priority Rules: Project commands > User commands (project command used when names are same) + +### Command Naming Rules + +#### File Path to Command Name Mapping Table + +| File Location | Generated Command | Example Call | +| ---------------------------- | ----------------- | --------------------- | +| `~/.qwen/commands/test.toml` | `/test` | `/test Parameter` | +| `/git/commit.toml` | `/git:commit` | `/git:commit Message` | + +Naming Rules: Path separator (`/` or `\`) converted to colon (`:`) + +### TOML File Format Specification + +| Field | Required | Description | Example | +| ------------- | -------- | ---------------------------------------- | ------------------------------------------ | +| `prompt` | Required | Prompt content sent to model | `prompt = "Please analyze code: {{args}}"` | +| `description` | Optional | Command description (displayed in /help) | `description = "Code analysis tool"` | + +### Parameter Processing Mechanism + +| Processing Method | Syntax | Applicable Scenarios | Security Features | +| ---------------------------- | ------------------ | ------------------------------------ | -------------------------------------- | +| Context-aware Injection | `{{args}}` | Need precise parameter control | Automatic Shell escaping | +| Default Parameter Processing | No special marking | Simple commands, parameter appending | Append as-is | +| Shell Command Injection | `!{command}` | Need dynamic content | Execution confirmation required before | + +#### 1. Context-aware Injection (`{{args}}`) + +| Scenario | TOML Configuration | Call Method | Actual Effect | +| ---------------- | --------------------------------------- | --------------------- | ------------------------ | +| Raw Injection | `prompt = "Fix: {{args}}"` | `/fix "Button issue"` | `Fix: "Button issue"` | +| In Shell Command | `prompt = "Search: !{grep {{args}} .}"` | `/search "hello"` | Execute `grep "hello" .` | + +#### 2. Default Parameter Processing + +| Input Situation | Processing Method | Example | +| --------------- | ------------------------------------------------------ | ---------------------------------------------- | +| Has parameters | Append to end of prompt (separated by two line breaks) | `/cmd parameter` → Original prompt + parameter | +| No parameters | Send prompt as is | `/cmd` → Original prompt | + +🚀 Dynamic Content Injection + +| Injection Type | Syntax | Processing Order | Purpose | +| --------------------- | -------------- | ------------------- | -------------------------------- | +| File Content | `@{file path}` | Processed first | Inject static reference files | +| Shell Commands | `!{command}` | Processed in middle | Inject dynamic execution results | +| Parameter Replacement | `{{args}}` | Processed last | Inject user parameters | + +#### 3. Shell Command Execution (`!{...}`) + +| Operation | User Interaction | +| ------------------------------- | -------------------- | +| 1. Parse command and parameters | - | +| 2. Automatic Shell escaping | - | +| 3. Show confirmation dialog | ✅ User confirmation | +| 4. Execute command | - | +| 5. Inject output to prompt | - | + +Example: Git Commit Message Generation + +``` +# git/commit.toml +description = "Generate Commit message based on staged changes" +prompt = """ +Please generate a Commit message based on the following diff: +diff +!{git diff --staged} +""" +``` + +#### 4. File Content Injection (`@{...}`) + +| File Type | Support Status | Processing Method | +| ------------ | ---------------------- | --------------------------- | +| Text Files | ✅ Full Support | Directly inject content | +| Images/PDF | ✅ Multi-modal Support | Encode and inject | +| Binary Files | ⚠️ Limited Support | May be skipped or truncated | +| Directory | ✅ Recursive Injection | Follow .gitignore rules | + +Example: Code Review Command + +``` +# review.toml +description = "Code review based on best practices" +prompt = """ +Review {{args}}, reference standards: + +@{docs/code-standards.md} +""" +``` + +### Practical Creation Example + +#### "Pure Function Refactoring" Command Creation Steps Table + +| Operation | Command/Code | +| ----------------------------- | ------------------------------------------- | +| 1. Create directory structure | `mkdir -p ~/.qwen/commands/refactor` | +| 2. Create command file | `touch ~/.qwen/commands/refactor/pure.toml` | +| 3. Edit command content | Refer to the complete code below. | +| 4. Test command | `@file.js` → `/refactor:pure` | + +```# ~/.qwen/commands/refactor/pure.toml +description = "Refactor code to pure function" +prompt = """ + Please analyze code in current context, refactor to pure function. + Requirements: + 1. Provide refactored code + 2. Explain key changes and pure function characteristic implementation + 3. Maintain function unchanged + """ +``` + +### Custom Command Best Practices Summary + +#### Command Design Recommendations Table + +| Practice Points | Recommended Approach | Avoid | +| -------------------- | ----------------------------------- | ------------------------------------------- | +| Command Naming | Use namespaces for organization | Avoid overly generic names | +| Parameter Processing | Clearly use `{{args}}` | Rely on default appending (easy to confuse) | +| Error Handling | Utilize Shell error output | Ignore execution failure | +| File Organization | Organize by function in directories | All commands in root directory | +| Description Field | Always provide clear description | Rely on auto-generated description | + +#### Security Features Reminder Table + +| Security Mechanism | Protection Effect | User Operation | +| ---------------------- | -------------------------- | ---------------------- | +| Shell Escaping | Prevent command injection | Automatic processing | +| Execution Confirmation | Avoid accidental execution | Dialog confirmation | +| Error Reporting | Help diagnose issues | View error information | diff --git a/docs/features/headless.md b/docs/users/features/headless.md similarity index 86% rename from docs/features/headless.md rename to docs/users/features/headless.md index 67d9decc..2a92bb1b 100644 --- a/docs/features/headless.md +++ b/docs/users/features/headless.md @@ -4,31 +4,6 @@ Headless mode allows you to run Qwen Code programmatically from command line scripts and automation tools without any interactive UI. This is ideal for scripting, automation, CI/CD pipelines, and building AI-powered tools. -- [Headless Mode](#headless-mode) - - [Overview](#overview) - - [Basic Usage](#basic-usage) - - [Direct Prompts](#direct-prompts) - - [Stdin Input](#stdin-input) - - [Combining with File Input](#combining-with-file-input) - - [Output Formats](#output-formats) - - [Text Output (Default)](#text-output-default) - - [JSON Output](#json-output) - - [Example Usage](#example-usage) - - [Stream-JSON Output](#stream-json-output) - - [Input Format](#input-format) - - [File Redirection](#file-redirection) - - [Configuration Options](#configuration-options) - - [Examples](#examples) - - [Code review](#code-review) - - [Generate commit messages](#generate-commit-messages) - - [API documentation](#api-documentation) - - [Batch code analysis](#batch-code-analysis) - - [PR code review](#pr-code-review) - - [Log analysis](#log-analysis) - - [Release notes generation](#release-notes-generation) - - [Model and tool usage tracking](#model-and-tool-usage-tracking) - - [Resources](#resources) - ## Overview The headless mode provides a headless interface to Qwen Code that: @@ -78,10 +53,10 @@ qwen --continue -p "Run the tests again and summarize failures" qwen --resume 123e4567-e89b-12d3-a456-426614174000 -p "Apply the follow-up refactor" ``` -Notes: - -- Session data is project-scoped JSONL under `~/.qwen/projects//chats`. -- Restores conversation history, tool outputs, and chat-compression checkpoints before sending the new prompt. +> [!note] +> +> - Session data is project-scoped JSONL under `~/.qwen/projects//chats`. +> - Restores conversation history, tool outputs, and chat-compression checkpoints before sending the new prompt. ## Output Formats @@ -228,7 +203,7 @@ Key command-line options for headless usage: | `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` | | `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` | -For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](./cli/configuration.md). +For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](/users/configuration/settings). ## Examples @@ -301,7 +276,7 @@ tail -5 usage.log ## Resources -- [CLI Configuration](./cli/configuration.md) - Complete configuration guide -- [Authentication](./cli/authentication.md) - Setup authentication -- [Commands](./cli/commands.md) - Interactive commands reference -- [Tutorials](./cli/tutorials.md) - Step-by-step automation guides +- [CLI Configuration](/users/configuration/settings#command-line-arguments) - Complete configuration guide +- [Authentication](/users/configuration/settings#environment-variables-for-api-access) - Setup authentication +- [Commands](/users/reference/cli-reference) - Interactive commands reference +- [Tutorials](/users/quickstart) - Step-by-step automation guides diff --git a/docs/users/features/mcp.md b/docs/users/features/mcp.md new file mode 100644 index 00000000..77fcea45 --- /dev/null +++ b/docs/users/features/mcp.md @@ -0,0 +1,284 @@ +# Connect Qwen Code to tools via MCP + +Qwen Code can connect to external tools and data sources through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction). MCP servers give Qwen Code access to your tools, databases, and APIs. + +## What you can do with MCP + +With MCP servers connected, you can ask Qwen Code to: + +- Work with files and repos (read/search/write, depending on the tools you enable) +- Query databases (schema inspection, queries, reporting) +- Integrate internal services (wrap your APIs as MCP tools) +- Automate workflows (repeatable tasks exposed as tools/prompts) + +> [!tip] +> If you’re looking for the “one command to get started”, jump to [Quick start](#quick-start). + +## Quick start + +Qwen Code loads MCP servers from `mcpServers` in your `settings.json`. You can configure servers either: + +- By editing `settings.json` directly +- By using `qwen mcp` commands (see [CLI reference](#qwen-mcp-cli)) + +### Add your first server + +1. Add a server (example: remote HTTP MCP server): + +```bash +qwen mcp add --transport http my-server http://localhost:3000/mcp +``` + +2. Verify it shows up: + +```bash +qwen mcp list +``` + +3. Restart Qwen Code in the same project (or start it if it wasn’t running yet), then ask the model to use tools from that server. + +## Where configuration is stored (scopes) + +Most users only need these two scopes: + +- **Project scope (default)**: `.qwen/settings.json` in your project root +- **User scope**: `~/.qwen/settings.json` across all projects on your machine + +Write to user scope: + +```bash +qwen mcp add --scope user --transport http my-server http://localhost:3000/mcp +``` + +> [!tip] +> For advanced configuration layers (system defaults/system settings and precedence rules), see [Settings](/users/configuration/settings). + +## Configure servers + +### Choose a transport + +| Transport | When to use | JSON field(s) | +| --------- | ----------------------------------------------------------------- | ------------------------------------------- | +| `http` | Recommended for remote services; works well for cloud MCP servers | `httpUrl` (+ optional `headers`) | +| `sse` | Legacy/deprecated servers that only support Server-Sent Events | `url` (+ optional `headers`) | +| `stdio` | Local process (scripts, CLIs, Docker) on your machine | `command`, `args` (+ optional `cwd`, `env`) | + +> [!note] +> If a server supports both, prefer **HTTP** over **SSE**. + +### Configure via `settings.json` vs `qwen mcp add` + +Both approaches produce the same `mcpServers` entries in your `settings.json`—use whichever you prefer. + +#### Stdio server (local process) + +JSON (`.qwen/settings.json`): + +```json +{ + "mcpServers": { + "pythonTools": { + "command": "python", + "args": ["-m", "my_mcp_server", "--port", "8080"], + "cwd": "./mcp-servers/python", + "env": { + "DATABASE_URL": "$DB_CONNECTION_STRING", + "API_KEY": "${EXTERNAL_API_KEY}" + }, + "timeout": 15000 + } + } +} +``` + +CLI (writes to project scope by default): + +```bash +qwen mcp add pythonTools -e DATABASE_URL=$DB_CONNECTION_STRING -e API_KEY=$EXTERNAL_API_KEY \ + --timeout 15000 python -m my_mcp_server --port 8080 +``` + +#### HTTP server (remote streamable HTTP) + +JSON: + +```json +{ + "mcpServers": { + "httpServerWithAuth": { + "httpUrl": "http://localhost:3000/mcp", + "headers": { + "Authorization": "Bearer your-api-token" + }, + "timeout": 5000 + } + } +} +``` + +CLI: + +```bash +qwen mcp add --transport http httpServerWithAuth http://localhost:3000/mcp \ + --header "Authorization: Bearer your-api-token" --timeout 5000 +``` + +#### SSE server (remote Server-Sent Events) + +JSON: + +```json +{ + "mcpServers": { + "sseServer": { + "url": "http://localhost:8080/sse", + "timeout": 30000 + } + } +} +``` + +CLI: + +```bash +qwen mcp add --transport sse sseServer http://localhost:8080/sse --timeout 30000 +``` + +## Safety and control + +### Trust (skip confirmations) + +- **Server trust** (`trust: true`): bypasses confirmation prompts for that server (use sparingly). + +### Tool filtering (allow/deny tools per server) + +Use `includeTools` / `excludeTools` to restrict tools exposed by a server (from Qwen Code’s perspective). + +Example: include only a few tools: + +```json +{ + "mcpServers": { + "filteredServer": { + "command": "python", + "args": ["-m", "my_mcp_server"], + "includeTools": ["safe_tool", "file_reader", "data_processor"], + "timeout": 30000 + } + } +} +``` + +### Global allow/deny lists + +The `mcp` object in your `settings.json` defines global rules for all MCP servers: + +- `mcp.allowed`: allow-list of MCP server names (keys in `mcpServers`) +- `mcp.excluded`: deny-list of MCP server names + +Example: + +```json +{ + "mcp": { + "allowed": ["my-trusted-server"], + "excluded": ["experimental-server"] + } +} +``` + +## Troubleshooting + +- **Server shows “Disconnected” in `qwen mcp list`**: verify the URL/command is correct, then increase `timeout`. +- **Stdio server fails to start**: use an absolute `command` path, and double-check `cwd`/`env`. +- **Environment variables in JSON don’t resolve**: ensure they exist in the environment where Qwen Code runs (shell vs GUI app environments can differ). + +## Reference + +### `settings.json` structure + +#### Server-specific configuration (`mcpServers`) + +Add an `mcpServers` object to your `settings.json` file: + +```json +// ... file contains other config objects +{ + "mcpServers": { + "serverName": { + "command": "path/to/server", + "args": ["--arg1", "value1"], + "env": { + "API_KEY": "$MY_API_TOKEN" + }, + "cwd": "./server-directory", + "timeout": 30000, + "trust": false + } + } +} +``` + +Configuration properties: + +Required (one of the following): + +| Property | Description | +| --------- | ------------------------------------------------------ | +| `command` | Path to the executable for Stdio transport | +| `url` | SSE endpoint URL (e.g., `"http://localhost:8080/sse"`) | +| `httpUrl` | HTTP streaming endpoint URL | + +Optional: + +| Property | Type/Default | Description | +| ---------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `args` | array | Command-line arguments for Stdio transport | +| `headers` | object | Custom HTTP headers when using `url` or `httpUrl` | +| `env` | object | Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax | +| `cwd` | string | Working directory for Stdio transport | +| `timeout` | number
(default: 600,000) | Request timeout in milliseconds (default: 600,000ms = 10 minutes) | +| `trust` | boolean
(default: false) | When `true`, bypasses all tool call confirmations for this server (default: `false`) | +| `includeTools` | array | 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. | +| `excludeTools` | array | 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. | +| `targetAudience` | string | The OAuth Client ID allowlisted on the IAP-protected application you are trying to access. Used with `authProviderType: 'service_account_impersonation'`. | +| `targetServiceAccount` | string | The email address of the Google Cloud Service Account to impersonate. Used with `authProviderType: 'service_account_impersonation'`. | + + + +### Manage MCP servers with `qwen mcp` + +You can always configure MCP servers by manually editing `settings.json`, but the CLI is usually faster. + +#### Adding a server (`qwen mcp add`) + +```bash +qwen mcp add [options] [args...] +``` + +| Argument/Option | Description | Default | Example | +| ------------------- | ------------------------------------------------------------------- | ------------------ | ----------------------------------------- | +| `` | A unique name for the server. | — | `example-server` | +| `` | The command to execute (for `stdio`) or the URL (for `http`/`sse`). | — | `/usr/bin/python` or `http://localhost:8` | +| `[args...]` | Optional arguments for a `stdio` command. | — | `--port 5000` | +| `-s`, `--scope` | Configuration scope (user or project). | `project` | `-s user` | +| `-t`, `--transport` | Transport type (`stdio`, `sse`, `http`). | `stdio` | `-t sse` | +| `-e`, `--env` | Set environment variables. | — | `-e KEY=value` | +| `-H`, `--header` | Set HTTP headers for SSE and HTTP transports. | — | `-H "X-Api-Key: abc123"` | +| `--timeout` | Set connection timeout in milliseconds. | — | `--timeout 30000` | +| `--trust` | Trust the server (bypass all tool call confirmation prompts). | — (`false`) | `--trust` | +| `--description` | Set the description for the server. | — | `--description "Local tools"` | +| `--include-tools` | A comma-separated list of tools to include. | all tools included | `--include-tools mytool,othertool` | +| `--exclude-tools` | A comma-separated list of tools to exclude. | none | `--exclude-tools mytool` | + +#### Listing servers (`qwen mcp list`) + +```bash +qwen mcp list +``` + +#### Removing a server (`qwen mcp remove`) + +```bash +qwen mcp remove +``` diff --git a/docs/users/features/sandbox.md b/docs/users/features/sandbox.md new file mode 100644 index 00000000..4d4e8f25 --- /dev/null +++ b/docs/users/features/sandbox.md @@ -0,0 +1,225 @@ +# Sandbox + +This document explains how to run Qwen Code inside a sandbox to reduce risk when tools execute shell commands or modify files. + +## Prerequisites + +Before using sandboxing, you need to install and set up Qwen Code: + +```bash +npm install -g @qwen-code/qwen-code +``` + +To verify the installation + +```bash +qwen --version +``` + +## Overview of sandboxing + +Sandboxing isolates potentially dangerous operations (such as shell commands or file modifications) from your host system, providing a security barrier between the CLI and your environment. + +The benefits of sandboxing include: + +- **Security**: Prevent accidental system damage or data loss. +- **Isolation**: Limit file system access to project directory. +- **Consistency**: Ensure reproducible environments across different systems. +- **Safety**: Reduce risk when working with untrusted code or experimental commands. + +> [!note] +> +> **Naming note:** Some sandbox-related environment variables still use the `GEMINI_*` prefix for backwards compatibility. + +## Sandboxing methods + +Your ideal method of sandboxing may differ depending on your platform and your preferred container solution. + +### 1. macOS Seatbelt (macOS only) + +Lightweight, built-in sandboxing using `sandbox-exec`. + +**Default profile**: `permissive-open` - restricts writes outside the project directory, but allows most other operations and outbound network access. + +**Best for**: Fast, no Docker required, strong guardrails for file writes. + +### 2. Container-based (Docker/Podman) + +Cross-platform sandboxing with complete process isolation. + +By default, Qwen Code uses a published sandbox image (configured in the CLI package) and will pull it as needed. + +**Best for**: Strong isolation on any OS, consistent tooling inside a known image. + +### Choosing a method + +- **On macOS**: + - Use Seatbelt when you want lightweight sandboxing (recommended for most users). + - Use Docker/Podman when you need a full Linux userland (e.g., tools that require Linux binaries). +- **On Linux/Windows**: + - Use Docker or Podman. + +## Quickstart + +```bash +# Enable sandboxing with command flag +qwen -s -p "analyze the code structure" + +# Or enable sandboxing for your shell session (recommended for CI / scripts) +export GEMINI_SANDBOX=true # true auto-picks a provider (see notes below) +qwen -p "run the test suite" + +# Configure in settings.json +{ + "tools": { + "sandbox": true + } +} +``` + +> [!tip] +> +> **Provider selection notes:** +> +> - On **macOS**, `GEMINI_SANDBOX=true` typically selects `sandbox-exec` (Seatbelt) if available. +> - On **Linux/Windows**, `GEMINI_SANDBOX=true` requires `docker` or `podman` to be installed. +> - To force a provider, set `GEMINI_SANDBOX=docker|podman|sandbox-exec`. + +## Configuration + +### Enable sandboxing (in order of precedence) + +1. **Environment variable**: `GEMINI_SANDBOX=true|false|docker|podman|sandbox-exec` +2. **Command flag / argument**: `-s`, `--sandbox`, or `--sandbox=` +3. **Settings file**: `tools.sandbox` in your `settings.json` (e.g., `{"tools": {"sandbox": true}}`). + +> [!important] +> +> If `GEMINI_SANDBOX` is set, it **overrides** the CLI flag and `settings.json`. + +### Configure the sandbox image (Docker/Podman) + +- **CLI flag**: `--sandbox-image ` +- **Environment variable**: `GEMINI_SANDBOX_IMAGE=` + +If you don’t set either, Qwen Code uses the default image configured in the CLI package (for example `ghcr.io/qwenlm/qwen-code:`). + +### macOS Seatbelt profiles + +Built-in profiles (set via `SEATBELT_PROFILE` env var): + +- `permissive-open` (default): Write restrictions, network allowed +- `permissive-closed`: Write restrictions, no network +- `permissive-proxied`: Write restrictions, network via proxy +- `restrictive-open`: Strict restrictions, network allowed +- `restrictive-closed`: Maximum restrictions +- `restrictive-proxied`: Strict restrictions, network via proxy + +> [!tip] +> +> Start with `permissive-open`, then tighten to `restrictive-closed` if your workflow still works. + +### Custom Seatbelt profiles (macOS) + +To use a custom Seatbelt profile: + +1. Create a file named `.qwen/sandbox-macos-.sb` in your project. +2. Set `SEATBELT_PROFILE=`. + +### Custom Sandbox Flags + +For container-based sandboxing, you can inject custom flags into the `docker` or `podman` command using the `SANDBOX_FLAGS` environment variable. This is useful for advanced configurations, such as disabling security features for specific use cases. + +**Example (Podman)**: + +To disable SELinux labeling for volume mounts, you can set the following: + +```bash +export SANDBOX_FLAGS="--security-opt label=disable" +``` + +Multiple flags can be provided as a space-separated string: + +```bash +export SANDBOX_FLAGS="--flag1 --flag2=value" +``` + +### Network proxying (all sandbox methods) + +If you want to restrict outbound network access to an allowlist, you can run a local proxy alongside the sandbox: + +- Set `GEMINI_SANDBOX_PROXY_COMMAND=` +- The command must start a proxy server that listens on `:::8877` + +This is especially useful with `*-proxied` Seatbelt profiles. + +For a working allowlist-style proxy example, see: [Example Proxy Script](/developers/examples/proxy-script). + +## Linux UID/GID handling + +The sandbox automatically handles user permissions on Linux. Override these permissions with: + +```bash +export SANDBOX_SET_UID_GID=true # Force host UID/GID +export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping +``` + +## Customizing the sandbox environment (Docker/Podman) + +If you need extra tools inside the container (e.g., `git`, `python`, `rg`), create a custom Dockerfile: + +- Path: `.qwen/sandbox.Dockerfile` +- Then run with: `BUILD_SANDBOX=1 qwen -s ...` + +This builds a project-specific image based on the default sandbox image. + +## Troubleshooting + +### Common issues + +**"Operation not permitted"** + +- Operation requires access outside sandbox. +- On macOS Seatbelt: try a more permissive `SEATBELT_PROFILE`. +- On Docker/Podman: verify the workspace is mounted and your command doesn’t require access outside the project directory. + +**Missing commands** + +- Container sandbox: add them via `.qwen/sandbox.Dockerfile` or `.qwen/sandbox.bashrc`. +- Seatbelt: your host binaries are used, but the sandbox may restrict access to some paths. + +**Network issues** + +- Check sandbox profile allows network. +- Verify proxy configuration. + +### Debug mode + +```bash +DEBUG=1 qwen -s -p "debug command" +``` + +**Note:** If you have `DEBUG=true` in a project's `.env` file, it won't affect the CLI due to automatic exclusion. Use `.qwen/.env` files for Qwen Code-specific debug settings. + +### Inspect sandbox + +```bash +# Check environment +qwen -s -p "run shell command: env | grep SANDBOX" + +# List mounts +qwen -s -p "run shell command: mount | grep workspace" +``` + +## Security notes + +- Sandboxing reduces but doesn't eliminate all risks. +- Use the most restrictive profile that allows your work. +- Container overhead is minimal after the first pull/build. +- GUI applications may not work in sandboxes. + +## Related documentation + +- [Configuration](/users/configuration/settings): Full configuration options. +- [Commands](/users/reference/cli-reference): Available commands. +- [Troubleshooting](/users/support/troubleshooting): General troubleshooting. diff --git a/docs/features/subagents.md b/docs/users/features/sub-agents.md similarity index 75% rename from docs/features/subagents.md rename to docs/users/features/sub-agents.md index 506d856f..3497df09 100644 --- a/docs/features/subagents.md +++ b/docs/users/features/sub-agents.md @@ -6,11 +6,11 @@ Subagents are specialized AI assistants that handle specific types of tasks with Subagents are independent AI assistants that: -- **Specialize in specific tasks** - Each subagent is configured with a focused system prompt for particular types of work -- **Have separate context** - They maintain their own conversation history, separate from your main chat -- **Use controlled tools** - You can configure which tools each subagent has access to -- **Work autonomously** - Once given a task, they work independently until completion or failure -- **Provide detailed feedback** - You can see their progress, tool usage, and execution statistics in real-time +- **Specialize in specific tasks** - Each Subagent is configured with a focused system prompt for particular types of work +- **Have separate context** - They maintain their own conversation history, separate from your main chat +- **Use controlled tools** - You can configure which tools each Subagent has access to +- **Work autonomously** - Once given a task, they work independently until completion or failure +- **Provide detailed feedback** - You can see their progress, tool usage, and execution statistics in real-time ## Key Benefits @@ -22,8 +22,8 @@ Subagents are independent AI assistants that: ## How Subagents Work -1. **Configuration**: You create subagent configurations that define their behavior, tools, and system prompts -2. **Delegation**: The main AI can automatically delegate tasks to appropriate subagents +1. **Configuration**: You create Subagents configurations that define their behavior, tools, and system prompts +2. **Delegation**: The main AI can automatically delegate tasks to appropriate Subagents 3. **Execution**: Subagents work independently, using their configured tools to complete tasks 4. **Results**: They return results and execution summaries back to the main conversation @@ -31,68 +31,46 @@ Subagents are independent AI assistants that: ### Quick Start -1. **Create your first subagent**: +1. **Create your first Subagent**: - ``` - /agents create - ``` + `/agents create` Follow the guided wizard to create a specialized agent. 2. **Manage existing agents**: - ``` - /agents manage - ``` + `/agents manage` - View and manage your configured subagents. + View and manage your configured Subagents. -3. **Use subagents automatically**: - Simply ask the main AI to perform tasks that match your subagents' specializations. The AI will automatically delegate appropriate work. +3. **Use Subagents automatically**: Simply ask the main AI to perform tasks that match your Subagents' specializations. The AI will automatically delegate appropriate work. ### Example Usage ``` User: "Please write comprehensive tests for the authentication module" - -AI: I'll delegate this to your testing specialist subagent. -[Delegates to "testing-expert" subagent] +AI: I'll delegate this to your testing specialist Subagents. +[Delegates to "testing-expert" Subagents] [Shows real-time progress of test creation] -[Returns with completed test files and execution summary] +[Returns with completed test files and execution summary]` ``` ## Management ### CLI Commands -Subagents are managed through the `/agents` slash command and its subcommands: +Subagents are managed through the `/agents` slash command and its subcommands: -#### `/agents create` +**Usage:**:`/agents create`。Creates a new Subagent through a guided step wizard. -Creates a new subagent through a guided step wizard. - -**Usage:** - -``` -/agents create -``` - -#### `/agents manage` - -Opens an interactive management dialog for viewing and managing existing subagents. - -**Usage:** - -``` -/agents manage -``` +**Usage:**:`/agents manage`。Opens an interactive management dialog for viewing and managing existing Subagents. ### Storage Locations Subagents are stored as Markdown files in two locations: -- **Project-level**: `.qwen/agents/` (takes precedence) -- **User-level**: `~/.qwen/agents/` (fallback) +- **Project-level**: `.qwen/agents/` (takes precedence) +- **User-level**: `~/.qwen/agents/` (fallback) This allows you to have both project-specific agents and personal agents that work across all projects. @@ -102,14 +80,14 @@ Subagents are configured using Markdown files with YAML frontmatter. This format #### Basic Structure -```markdown +``` --- name: agent-name description: Brief description of when and how to use this agent tools: - - tool1 - - tool2 - - tool3 # Optional + - tool1 + - tool2 + - tool3 # Optional --- System prompt content goes here. @@ -119,7 +97,7 @@ You can use ${variable} templating for dynamic content. #### Example Usage -```markdown +``` --- name: project-documenter description: Creates project documentation and README files @@ -143,19 +121,19 @@ new contributors and end users understand the project. Qwen Code proactively delegates tasks based on: - The task description in your request -- The description field in subagent configurations +- The description field in Subagents configurations - Current context and available tools -To encourage more proactive subagent use, include phrases like "use PROACTIVELY" or "MUST BE USED" in your description field. +To encourage more proactive Subagents use, include phrases like "use PROACTIVELY" or "MUST BE USED" in your description field. ### Explicit Invocation -Request a specific subagent by mentioning it in your command: +Request a specific Subagent by mentioning it in your command: ``` -> Let the testing-expert subagent create unit tests for the payment module -> Have the documentation-writer subagent update the API reference -> Get the react-specialist subagent to optimize this component's performance +Let the testing-expert Subagents create unit tests for the payment module +Have the documentation-writer Subagents update the API reference +Get the react-specialist Subagents to optimize this component's performance ``` ## Examples @@ -166,7 +144,7 @@ Request a specific subagent by mentioning it in your command: Perfect for comprehensive test creation and test-driven development. -```markdown +``` --- name: testing-expert description: Writes comprehensive unit tests, integration tests, and handles test automation with best practices @@ -202,15 +180,15 @@ Focus on both positive and negative test cases. **Use Cases:** -- "Write unit tests for the authentication service" -- "Create integration tests for the payment processing workflow" -- "Add test coverage for edge cases in the data validation module" +- “Write unit tests for the authentication service” +- “Create integration tests for the payment processing workflow” +- “Add test coverage for edge cases in the data validation module” #### Documentation Writer Specialized in creating clear, comprehensive documentation. -```markdown +``` --- name: documentation-writer description: Creates comprehensive documentation, README files, API docs, and user guides @@ -255,15 +233,15 @@ the actual implementation. Use clear headings, bullet points, and examples. **Use Cases:** -- "Create API documentation for the user management endpoints" -- "Write a comprehensive README for this project" -- "Document the deployment process with troubleshooting steps" +- “Create API documentation for the user management endpoints” +- “Write a comprehensive README for this project” +- “Document the deployment process with troubleshooting steps” #### Code Reviewer Focused on code quality, security, and best practices. -```markdown +``` --- name: code-reviewer description: Reviews code for best practices, security issues, performance, and maintainability @@ -297,9 +275,9 @@ Prioritize issues by impact and provide rationale for recommendations. **Use Cases:** -- "Review this authentication implementation for security issues" -- "Check the performance implications of this database query logic" -- "Evaluate the code structure and suggest improvements" +- “Review this authentication implementation for security issues” +- “Check the performance implications of this database query logic” +- “Evaluate the code structure and suggest improvements” ### Technology-Specific Agents @@ -307,7 +285,7 @@ Prioritize issues by impact and provide rationale for recommendations. Optimized for React development, hooks, and component patterns. -```markdown +``` --- name: react-specialist description: Expert in React development, hooks, component patterns, and modern React best practices @@ -344,15 +322,15 @@ Focus on accessibility and user experience considerations. **Use Cases:** -- "Create a reusable data table component with sorting and filtering" -- "Implement a custom hook for API data fetching with caching" -- "Refactor this class component to use modern React patterns" +- “Create a reusable data table component with sorting and filtering” +- “Implement a custom hook for API data fetching with caching” +- “Refactor this class component to use modern React patterns” #### Python Expert Specialized in Python development, frameworks, and best practices. -```markdown +``` --- name: python-expert description: Expert in Python development, frameworks, testing, and Python-specific best practices @@ -390,9 +368,9 @@ Focus on writing clean, maintainable Python code that follows community standard **Use Cases:** -- "Create a FastAPI service for user authentication with JWT tokens" -- "Implement a data processing pipeline with pandas and error handling" -- "Write a CLI tool using argparse with comprehensive help documentation" +- “Create a FastAPI service for user authentication with JWT tokens” +- “Implement a data processing pipeline with pandas and error handling” +- “Write a CLI tool using argparse with comprehensive help documentation” ## Best Practices @@ -400,11 +378,11 @@ Focus on writing clean, maintainable Python code that follows community standard #### Single Responsibility Principle -Each subagent should have a clear, focused purpose. +Each Subagent should have a clear, focused purpose. **✅ Good:** -```markdown +``` --- name: testing-expert description: Writes comprehensive unit tests and integration tests @@ -413,14 +391,14 @@ description: Writes comprehensive unit tests and integration tests **❌ Avoid:** -```markdown +``` --- name: general-helper description: Helps with testing, documentation, code review, and deployment --- ``` -**Why:** Focused agents produce better results and are easier to maintain. +**Why:** Focused agents produce better results and are easier to maintain. #### Clear Specialization @@ -428,7 +406,7 @@ Define specific expertise areas rather than broad capabilities. **✅ Good:** -```markdown +``` --- name: react-performance-optimizer description: Optimizes React applications for performance using profiling and best practices @@ -437,14 +415,14 @@ description: Optimizes React applications for performance using profiling and be **❌ Avoid:** -```markdown +``` --- name: frontend-developer description: Works on frontend development tasks --- ``` -**Why:** Specific expertise leads to more targeted and effective assistance. +**Why:** Specific expertise leads to more targeted and effective assistance. #### Actionable Descriptions @@ -452,17 +430,17 @@ Write descriptions that clearly indicate when to use the agent. **✅ Good:** -```markdown +``` description: Reviews code for security vulnerabilities, performance issues, and maintainability concerns ``` **❌ Avoid:** -```markdown +``` description: A helpful code reviewer ``` -**Why:** Clear descriptions help the main AI choose the right agent for each task. +**Why:** Clear descriptions help the main AI choose the right agent for each task. ### Configuration Best Practices @@ -470,7 +448,7 @@ description: A helpful code reviewer **Be Specific About Expertise:** -```markdown +``` You are a Python testing specialist with expertise in: - pytest framework and fixtures @@ -481,7 +459,7 @@ You are a Python testing specialist with expertise in: **Include Step-by-Step Approaches:** -```markdown +``` For each testing task: 1. Analyze the code structure and dependencies @@ -493,7 +471,7 @@ For each testing task: **Specify Output Standards:** -```markdown +``` Always follow these standards: - Use descriptive test names that explain the scenario @@ -506,7 +484,7 @@ Always follow these standards: - **Tool Restrictions**: Subagents only have access to their configured tools - **Sandboxing**: All tool execution follows the same security model as direct tool use -- **Audit Trail**: All subagent actions are logged and visible in real-time +- **Audit Trail**: All Subagents actions are logged and visible in real-time - **Access Control**: Project and user-level separation provides appropriate boundaries - **Sensitive Information**: Avoid including secrets or credentials in agent configurations - **Production Environments**: Consider separate agents for production vs development environments diff --git a/docs/users/features/token-caching.md b/docs/users/features/token-caching.md new file mode 100644 index 00000000..51381c94 --- /dev/null +++ b/docs/users/features/token-caching.md @@ -0,0 +1,29 @@ +# Token Caching and Cost Optimization + +Qwen Code automatically optimizes API costs through token caching when using API key authentication. This feature stores frequently used content like system instructions and conversation history to reduce the number of tokens processed in subsequent requests. + +## How It Benefits You + +- **Cost reduction**: Less tokens mean lower API costs +- **Faster responses**: Cached content is retrieved more quickly +- **Automatic optimization**: No configuration needed - it works behind the scenes + +## Token caching is available for + +- API key users (Qwen API key, OpenAI-compatible providers) + +## Monitoring Your Savings + +Use the `/stats` command to see your cached token savings: + +- When active, the stats display shows how many tokens were served from cache +- You'll see both the absolute number and percentage of cached tokens +- Example: "10,500 (90.4%) of input tokens were served from the cache, reducing costs." + +This information is only displayed when cached tokens are being used, which occurs with API key authentication but not with OAuth authentication. + +## Example Stats Display + +![Qwen Code Stats Display](https://img.alicdn.com/imgextra/i3/O1CN01F1yzRs1juyZu63jdS_!!6000000004609-2-tps-1038-738.png) + +The above image shows an example of the `/stats` command output, highlighting the cached token savings information. diff --git a/docs/ide-integration/_meta.ts b/docs/users/ide-integration/_meta.ts similarity index 100% rename from docs/ide-integration/_meta.ts rename to docs/users/ide-integration/_meta.ts diff --git a/docs/ide-integration/ide-companion-spec.md b/docs/users/ide-integration/ide-companion-spec.md similarity index 100% rename from docs/ide-integration/ide-companion-spec.md rename to docs/users/ide-integration/ide-companion-spec.md diff --git a/docs/ide-integration/ide-integration.md b/docs/users/ide-integration/ide-integration.md similarity index 99% rename from docs/ide-integration/ide-integration.md rename to docs/users/ide-integration/ide-integration.md index febcf478..af7d1b7a 100644 --- a/docs/ide-integration/ide-integration.md +++ b/docs/users/ide-integration/ide-integration.md @@ -2,7 +2,7 @@ Qwen Code can integrate with your IDE to provide a more seamless and context-aware experience. This integration allows the CLI to understand your workspace better and enables powerful features like native in-editor diffing. -Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](./ide-companion-spec.md). +Currently, the only supported IDE is [Visual Studio Code](https://code.visualstudio.com/) and other editors that support VS Code extensions. To build support for other editors, see the [IDE Companion Extension Spec](/users/ide-integration/ide-companion-spec). ## Features diff --git a/docs/users/integration-github-action.md b/docs/users/integration-github-action.md new file mode 100644 index 00000000..b9b348a1 --- /dev/null +++ b/docs/users/integration-github-action.md @@ -0,0 +1,282 @@ +# Github Actions:qwen-code-action + +## Overview + +`qwen-code-action` is a GitHub Action that integrates [Qwen Code] into your development workflow via the [Qwen Code CLI]. It acts both as an autonomous agent for critical routine coding tasks, and an on-demand collaborator you can quickly delegate work to. + +Use it to perform GitHub pull request reviews, triage issues, perform code analysis and modification, and more using [Qwen Code] conversationally (e.g., `@qwencoder fix this issue`) directly inside your GitHub repositories. + +- [qwen-code-action](#qwen-code-action) + - [Overview](#overview) + - [Features](#features) + - [Quick Start](#quick-start) + - [1. Get a Qwen API Key](#1-get-a-qwen-api-key) + - [2. Add it as a GitHub Secret](#2-add-it-as-a-github-secret) + - [3. Update your .gitignore](#3-update-your-gitignore) + - [4. Choose a Workflow](#4-choose-a-workflow) + - [5. Try it out](#5-try-it-out) + - [Workflows](#workflows) + - [Qwen Code Dispatch](#qwen-code-dispatch) + - [Issue Triage](#issue-triage) + - [Pull Request Review](#pull-request-review) + - [Qwen Code CLI Assistant](#qwen-code-cli-assistant) + - [Configuration](#configuration) + - [Inputs](#inputs) + - [Outputs](#outputs) + - [Repository Variables](#repository-variables) + - [Secrets](#secrets) + - [Authentication](#authentication) + - [GitHub Authentication](#github-authentication) + - [Extensions](#extensions) + - [Best Practices](#best-practices) + - [Customization](#customization) + - [Contributing](#contributing) + +## Features + +- **Automation**: Trigger workflows based on events (e.g. issue opening) or schedules (e.g. nightly). +- **On-demand Collaboration**: Trigger workflows in issue and pull request + comments by mentioning the [Qwen Code CLI] (e.g., `@qwencoder /review`). +- **Extensible with Tools**: Leverage [Qwen Code] models' tool-calling capabilities to + interact with other CLIs like the [GitHub CLI] (`gh`). +- **Customizable**: Use a `QWEN.md` file in your repository to provide + project-specific instructions and context to [Qwen Code CLI]. + +## Quick Start + +Get started with Qwen Code CLI in your repository in just a few minutes: + +### 1. Get a Qwen API Key + +Obtain your API key from [DashScope] (Alibaba Cloud's AI platform) + +### 2. Add it as a GitHub Secret + +Store your API key as a secret named `QWEN_API_KEY` in your repository: + +- Go to your repository's **Settings > Secrets and variables > Actions** +- Click **New repository secret** +- Name: `QWEN_API_KEY`, Value: your API key + +### 3. Update your .gitignore + +Add the following entries to your `.gitignore` file: + +```gitignore +# qwen-code-cli settings +.qwen/ + +# GitHub App credentials +gha-creds-*.json +``` + +### 4. Choose a Workflow + +You have two options to set up a workflow: + +**Option A: Use setup command (Recommended)** + +1. Start the Qwen Code CLI in your terminal: + + ```shell + qwen + ``` + +2. In Qwen Code CLI in your terminal, type: + + ``` + /setup-github + ``` + +**Option B: Manually copy workflows** + +1. Copy the pre-built workflows from the [`examples/workflows`](./examples/workflows) directory to your repository's `.github/workflows` directory. Note: the `qwen-dispatch.yml` workflow must also be copied, which triggers the workflows to run. + +### 5. Try it out + +**Pull Request Review:** + +- Open a pull request in your repository and wait for automatic review +- Comment `@qwencoder /review` on an existing pull request to manually trigger a review + +**Issue Triage:** + +- Open an issue and wait for automatic triage +- Comment `@qwencoder /triage` on existing issues to manually trigger triaging + +**General AI Assistance:** + +- In any issue or pull request, mention `@qwencoder` followed by your request +- Examples: + - `@qwencoder explain this code change` + - `@qwencoder suggest improvements for this function` + - `@qwencoder help me debug this error` + - `@qwencoder write unit tests for this component` + +## Workflows + +This action provides several pre-built workflows for different use cases. Each workflow is designed to be copied into your repository's `.github/workflows` directory and customized as needed. + +### Qwen Code Dispatch + +This workflow acts as a central dispatcher for Qwen Code CLI, routing requests to +the appropriate workflow based on the triggering event and the command provided +in the comment. For a detailed guide on how to set up the dispatch workflow, go +to the +[Qwen Code Dispatch workflow documentation](./examples/workflows/qwen-dispatch). + +### Issue Triage + +This action can be used to triage GitHub Issues automatically or on a schedule. +For a detailed guide on how to set up the issue triage system, go to the +[GitHub Issue Triage workflow documentation](./examples/workflows/issue-triage). + +### Pull Request Review + +This action can be used to automatically review pull requests when they are +opened. For a detailed guide on how to set up the pull request review system, +go to the [GitHub PR Review workflow documentation](./examples/workflows/pr-review). + +### Qwen Code CLI Assistant + +This type of action can be used to invoke a general-purpose, conversational Qwen Code +AI assistant within the pull requests and issues to perform a wide range of +tasks. For a detailed guide on how to set up the general-purpose Qwen Code CLI workflow, +go to the [Qwen Code Assistant workflow documentation](./examples/workflows/qwen-assistant). + +## Configuration + +### Inputs + + + +- qwen*api_key: *(Optional)\_ The API key for the Qwen API. + +- qwen*cli_version: *(Optional, default: `latest`)\_ The version of the Qwen Code CLI to install. Can be "latest", "preview", "nightly", a specific version number, or a git branch, tag, or commit. For more information, see [Qwen Code CLI releases](https://github.com/QwenLM/qwen-code-action/blob/main/docs/releases.md). + +- qwen*debug: *(Optional)\_ Enable debug logging and output streaming. + +- qwen*model: *(Optional)\_ The model to use with Qwen Code. + +- prompt: _(Optional, default: `You are a helpful assistant.`)_ A string passed to the Qwen Code CLI's [`--prompt` argument](https://github.com/QwenLM/qwen-code-action/blob/main/docs/cli/configuration.md#command-line-arguments). + +- settings: _(Optional)_ A JSON string written to `.qwen/settings.json` to configure the CLI's _project_ settings. + For more details, see the documentation on [settings files](https://github.com/QwenLM/qwen-code-action/blob/main/docs/cli/configuration.md#settings-files). + +- use*qwen_code_assist: *(Optional, default: `false`)\_ Whether to use Code Assist for Qwen Code model access instead of the default Qwen Code API key. + For more information, see the [Qwen Code CLI documentation](https://github.com/QwenLM/qwen-code-action/blob/main/docs/cli/authentication.md). + +- use*vertex_ai: *(Optional, default: `false`)\_ Whether to use Vertex AI for Qwen Code model access instead of the default Qwen Code API key. + For more information, see the [Qwen Code CLI documentation](https://github.com/QwenLM/qwen-code-action/blob/main/docs/cli/authentication.md). + +- extensions: _(Optional)_ A list of Qwen Code CLI extensions to install. + +- upload*artifacts: *(Optional, default: `false`)\_ Whether to upload artifacts to the github action. + +- use*pnpm: *(Optional, default: `false`)\_ Whether or not to use pnpm instead of npm to install qwen-code-cli + +- workflow*name: *(Optional, default: `${{ github.workflow }}`)\_ The GitHub workflow name, used for telemetry purposes. + + + +### Outputs + + + +- summary: The summarized output from the Qwen Code CLI execution. + +- error: The error output from the Qwen Code CLI execution, if any. + + + +### Repository Variables + +We recommend setting the following values as repository variables so they can be reused across all workflows. Alternatively, you can set them inline as action inputs in individual workflows or to override repository-level values. + +| Name | Description | Type | Required | When Required | +| ------------------ | --------------------------------------------------------- | -------- | -------- | ------------------------- | +| `DEBUG` | Enables debug logging for the Qwen Code CLI. | Variable | No | Never | +| `QWEN_CLI_VERSION` | Controls which version of the Qwen Code CLI is installed. | Variable | No | Pinning the CLI version | +| `APP_ID` | GitHub App ID for custom authentication. | Variable | No | Using a custom GitHub App | + +To add a repository variable: + +1. Go to your repository's **Settings > Secrets and variables > Actions > New variable**. +2. Enter the variable name and value. +3. Save. + +For details about repository variables, refer to the [GitHub documentation on variables][variables]. + +### Secrets + +You can set the following secrets in your repository: + +| Name | Description | Required | When Required | +| ----------------- | --------------------------------------------- | -------- | ------------------------------------------ | +| `QWEN_API_KEY` | Your Qwen API key from DashScope. | Yes | Required for all workflows that call Qwen. | +| `APP_PRIVATE_KEY` | Private key for your GitHub App (PEM format). | No | Using a custom GitHub App. | + +To add a secret: + +1. Go to your repository's **Settings > Secrets and variables >Actions > New repository secret**. +2. Enter the secret name and value. +3. Save. + +For more information, refer to the +[official GitHub documentation on creating and using encrypted secrets][secrets]. + +## Authentication + +This action requires authentication to the GitHub API and optionally to Qwen Code services. + +### GitHub Authentication + +You can authenticate with GitHub in two ways: + +1. **Default `GITHUB_TOKEN`:** For simpler use cases, the action can use the + default `GITHUB_TOKEN` provided by the workflow. +2. **Custom GitHub App (Recommended):** For the most secure and flexible + authentication, we recommend creating a custom GitHub App. + +For detailed setup instructions for both Qwen and GitHub authentication, go to the +[**Authentication documentation**](./docs/authentication.md). + +## Extensions + +The Qwen Code CLI can be extended with additional functionality through extensions. +These extensions are installed from source from their GitHub repositories. + +For detailed instructions on how to set up and configure extensions, go to the +[Extensions documentation](./docs/extensions.md). + +## Best Practices + +To ensure the security, reliability, and efficiency of your automated workflows, we strongly recommend following our best practices. These guidelines cover key areas such as repository security, workflow configuration, and monitoring. + +Key recommendations include: + +- **Securing Your Repository:** Implementing branch and tag protection, and restricting pull request approvers. +- **Monitoring and Auditing:** Regularly reviewing action logs and enabling OpenTelemetry for deeper insights into performance and behavior. + +For a comprehensive guide on securing your repository and workflows, please refer to our [**Best Practices documentation**](./docs/best-practices.md). + +## Customization + +Create a [QWEN.md] file in the root of your repository to provide +project-specific context and instructions to [Qwen Code CLI]. This is useful for defining +coding conventions, architectural patterns, or other guidelines the model should +follow for a given repository. + +## Contributing + +Contributions are welcome! Check out the Qwen Code CLI +[**Contributing Guide**](./CONTRIBUTING.md) for more details on how to get +started. + +[secrets]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions +[Qwen Code]: https://github.com/QwenLM/qwen-code +[DashScope]: https://dashscope.console.aliyun.com/apiKey +[Qwen Code CLI]: https://github.com/QwenLM/qwen-code-action/ +[variables]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables#creating-configuration-variables-for-a-repository +[GitHub CLI]: https://docs.github.com/en/github-cli/github-cli +[QWEN.md]: https://github.com/QwenLM/qwen-code-action/blob/main/docs/cli/configuration.md#context-files-hierarchical-instructional-context diff --git a/docs/users/integration-vscode.md b/docs/users/integration-vscode.md new file mode 100644 index 00000000..e827df26 --- /dev/null +++ b/docs/users/integration-vscode.md @@ -0,0 +1,45 @@ +# Visual Studio Code + +> The VS Code extension (Beta) lets you see Qwen's changes in real-time through a native graphical interface integrated directly into your IDE, making it easier to access and interact with Qwen Code. + +
+ + + +### Features + +- **Native IDE experience**: Dedicated Qwen Code sidebar panel accessed via the Qwen icon +- **Auto-accept edits mode**: Automatically apply Qwen's changes as they're made +- **File management**: @-mention files or attach files and images using the system file picker +- **Conversation history**: Access to past conversations +- **Multiple sessions**: Run multiple Qwen Code sessions simultaneously + +### Requirements + +- VS Code 1.98.0 or higher + +### Installation + +1. Install Qwen Code CLI: + + ```bash + npm install -g qwen-code + ``` + +2. Download and install the extension from the [Visual Studio Code Extension Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion). + +## Troubleshooting + +### Extension not installing + +- Ensure you have VS Code 1.98.0 or higher +- Check that VS Code has permission to install extensions +- Try installing directly from the Marketplace website + +### Qwen Code not responding + +- Check your internet connection +- Start a new conversation to see if the issue persists +- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem continues diff --git a/docs/users/integration-zed.md b/docs/users/integration-zed.md new file mode 100644 index 00000000..5a2fa7e5 --- /dev/null +++ b/docs/users/integration-zed.md @@ -0,0 +1,54 @@ +# Zed Editor + +> Zed Editor provides native support for AI coding assistants through the Agent Control Protocol (ACP). This integration allows you to use Qwen Code directly within Zed's interface with real-time code suggestions. + +![Zed Editor Overview](https://img.alicdn.com/imgextra/i1/O1CN01aAhU311GwEoNh27FP_!!6000000000686-2-tps-3024-1898.png) + +### Features + +- **Native agent experience**: Integrated AI assistant panel within Zed's interface +- **Agent Control Protocol**: Full support for ACP enabling advanced IDE interactions +- **File management**: @-mention files to add them to the conversation context +- **Conversation history**: Access to past conversations within Zed + +### Requirements + +- Zed Editor (latest version recommended) +- Qwen Code CLI installed + +### Installation + +1. Install Qwen Code CLI: + + ```bash + npm install -g qwen-code + ``` + +2. Download and install [Zed Editor](https://zed.dev/) + +3. In Zed, click the **settings button** in the top right corner, select **"Add agent"**, choose **"Create a custom agent"**, and add the following configuration: + +```json +"Qwen Code": { + "type": "custom", + "command": "qwen", + "args": ["--experimental-acp"], + "env": {} +} +``` + +![Qwen Code Integration](https://img.alicdn.com/imgextra/i1/O1CN013s61L91dSE1J7MTgO_!!6000000003734-2-tps-2592-1234.png) + +## Troubleshooting + +### Agent not appearing + +- Run `qwen --version` in terminal to verify installation +- Check that the JSON configuration is valid +- Restart Zed Editor + +### Qwen Code not responding + +- Check your internet connection +- Verify CLI works by running `qwen` in terminal +- [File an issue on GitHub](https://github.com/qwenlm/qwen-code/issues) if the problem persists diff --git a/docs/users/overview.md b/docs/users/overview.md new file mode 100644 index 00000000..5ae43303 --- /dev/null +++ b/docs/users/overview.md @@ -0,0 +1,62 @@ +# Qwen Code overview + +> Learn about Qwen Code, Qwen's agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before. + +## Get started in 30 seconds + +Prerequisites: + +- A [Qwen Code](https://chat.qwen.ai/auth?mode=register) account +- Requires [Node.js 20+](https://nodejs.org/zh-cn/download), you can use `node -v` to check the version. If it's not installed, use the following command to install it. + +### Install Qwen Code: + +**NPM**(recommended) + +```bash +npm install -g @qwen-code/qwen-code@latest +``` + +**Homebrew**(macOS, Linux) + +```bash +brew install qwen-code +``` + +### Start using Qwen Code: + +```bash +cd your-project +qwen +``` + +Select **Qwen OAuth (Free)** authentication and follow the prompts to log in. Then let's start with understanding your codebase. Try one of these commands: + +``` +what does this project do? +``` + +![](https://gw.alicdn.com/imgextra/i2/O1CN01XoPbZm1CrsZzvMQ6m_!!6000000000135-1-tps-772-646.gif) + +You'll be prompted to log in on first use. That's it! [Continue with Quickstart (5 mins) →](/users/quickstart) + +> [!tip] +> +> See [troubleshooting](/users/support/troubleshooting) if you hit issues. + +> [!note] +> +> **New VS Code Extension (Beta)**: Prefer a graphical interface? Our new **VS Code extension** provides an easy-to-use native IDE experience without requiring terminal familiarity. Simply install from the marketplace and start coding with Qwen Code directly in your sidebar. You can search for **Qwen Code** in the VS Code Marketplace and download it. + +## What Qwen Code does for you + +- **Build features from descriptions**: Tell Qwen Code what you want to build in plain language. It will make a plan, write the code, and ensure it works. +- **Debug and fix issues**: Describe a bug or paste an error message. Qwen Code will analyze your codebase, identify the problem, and implement a fix. +- **Navigate any codebase**: Ask anything about your team's codebase, and get a thoughtful answer back. Qwen Code maintains awareness of your entire project structure, can find up-to-date information from the web, and with [MCP](/users/features/mcp) can pull from external datasources like Google Drive, Figma, and Slack. +- **Automate tedious tasks**: Fix fiddly lint issues, resolve merge conflicts, and write release notes. Do all this in a single command from your developer machines, or automatically in CI. + +## Why developers love Qwen Code + +- **Works in your terminal**: Not another chat window. Not another IDE. Qwen Code meets you where you already work, with the tools you already love. +- **Takes action**: Qwen Code can directly edit files, run commands, and create commits. Need more? [MCP](/users/features/mcp) lets Qwen Code read your design docs in Google Drive, update your tickets in Jira, or use _your_ custom developer tooling. +- **Unix philosophy**: Qwen Code is composable and scriptable. `tail -f app.log | qwen -p "Slack me if you see any anomalies appear in this log stream"` _works_. Your CI can run `qwen -p "If there are new text strings, translate them into French and raise a PR for @lang-fr-team to review"`. diff --git a/docs/users/quickstart.md b/docs/users/quickstart.md new file mode 100644 index 00000000..2f4318c3 --- /dev/null +++ b/docs/users/quickstart.md @@ -0,0 +1,251 @@ +# Quickstart + +> 👏 Welcome to Qwen Code! + +This quickstart guide will have you using AI-powered coding assistance in just a few minutes. By the end, you'll understand how to use Qwen Code for common development tasks. + +## Before you begin + +Make sure you have: + +- A **terminal** or command prompt open +- A code project to work with +- A [Qwen Code](https://chat.qwen.ai/auth?mode=register) account + +## Step 1: Install Qwen Code + +To install Qwen Code, use one of the following methods: + +### NPM (recommended) + +Requires [Node.js 20+](https://nodejs.org/download), you can use `node -v` check the version. If it's not installed, use the following command to install it. + +If you have [Node.js or newer installed](https://nodejs.org/en/download/): + +```sh +npm install -g @qwen-code/qwen-code@latest +``` + +### Homebrew (macOS, Linux) + +```sh +brew install qwen-code +``` + +## Step 2: Log in to your account + +Qwen Code requires an account to use. When you start an interactive session with the `qwen` command, you'll need to log in: + +```bash +# You'll be prompted to log in on first use +qwen +``` + +```bash +# Follow the prompts to log in with your account +/auth +``` + +Select `Qwen OAuth`, log in to your account and follow the prompts to confirm. Once logged in, your credentials are stored and you won't need to log in again. + +> [!note] +> +> When you first authenticate Qwen Code with your Qwen account, a workspace called ".qwen" is automatically created for you. This workspace provides centralized cost tracking and management for all Qwen Code usage in your organization. + +> [!tip] +> +> If you need to log in again or switch accounts, use the `/auth` command within Qwen Code. + +## Step 3: Start your first session + +Open your terminal in any project directory and start Qwen Code: + +```bash +# optiona +cd /path/to/your/project +# start qwen +qwen +``` + +You'll see the Qwen Code welcome screen with your session information, recent conversations, and latest updates. Type `/help` for available commands. + +## Chat with Qwen Code + +### Ask your first question + +Qwen Code will analyze your files and provide a summary. You can also ask more specific questions: + +``` +explain the folder structure +``` + +You can also ask Qwen Code about its own capabilities: + +``` +what can Qwen Code do? +``` + +> [!note] +> +> Qwen Code reads your files as needed - you don't have to manually add context. Qwen Code also has access to its own documentation and can answer questions about its features and capabilities. + +### Make your first code change + +Now let's make Qwen Code do some actual coding. Try a simple task: + +``` +add a hello world function to the main file +``` + +Qwen Code will: + +1. Find the appropriate file +2. Show you the proposed changes +3. Ask for your approval +4. Make the edit + +> [!note] +> +> Qwen Code always asks for permission before modifying files. You can approve individual changes or enable "Accept all" mode for a session. + +### Use Git with Qwen Code + +Qwen Code makes Git operations conversational: + +``` +what files have I changed? +``` + +``` +commit my changes with a descriptive message +``` + +You can also prompt for more complex Git operations: + +``` +create a new branch called feature/quickstart +``` + +``` +show me the last 5 commits +``` + +``` +help me resolve merge conflicts +``` + +### Fix a bug or add a feature + +Qwen Code is proficient at debugging and feature implementation. + +Describe what you want in natural language: + +``` +add input validation to the user registration form +``` + +Or fix existing issues: + +``` +there's a bug where users can submit empty forms - fix it +``` + +Qwen Code will: + +- Locate the relevant code +- Understand the context +- Implement a solution +- Run tests if available + +### Test out other common workflows + +There are a number of ways to work with Claude: + +**Refactor code** + +``` +refactor the authentication module to use async/await instead of callbacks +``` + +**Write tests** + +``` +write unit tests for the calculator functions +``` + +**Update documentation** + +``` +update the README with installation instructions +``` + +**Code review** + +``` +review my changes and suggest improvements +``` + +> [!tip] +> +> **Remember**: Qwen Code is your AI pair programmer. Talk to it like you would a helpful colleague - describe what you want to achieve, and it will help you get there. + +## Essential commands + +Here are the most important commands for daily use: + +| Command | What it does | Example | +| --------------------- | ------------------------------------------------ | ----------------------------- | +| `qwen` | start Qwen Code | `qwen` | +| `/auth` | Change authentication method | `/auth` | +| `/help` | Display help information for available commands | `/help` or `/?` | +| `/compress` | Replace chat history with summary to save Tokens | `/compress` | +| `/clear` | Clear terminal screen content | `/clear` (shortcut: `Ctrl+L`) | +| `/theme` | Change Qwen Code visual theme | `/theme` | +| `/language` | View or change language settings | `/language` | +| → `ui [language]` | Set UI interface language | `/language ui zh-CN` | +| → `output [language]` | Set LLM output language | `/language output Chinese` | +| `/quit` | Exit Qwen Code immediately | `/quit` or `/exit` | + +See the [CLI reference](/users/reference/cli-reference) for a complete list of commands. + +## Pro tips for beginners + +**Be specific with your requests** + +- Instead of: "fix the bug" +- Try: "fix the login bug where users see a blank screen after entering wrong credentials" + +**Use step-by-step instructions** + +- Break complex tasks into steps: + +``` +1. create a new database table for user profiles +2. create an API endpoint to get and update user profiles +3. build a webpage that allows users to see and edit their information +``` + +**Let Claude explore first** + +- Before making changes, let Claude understand your code: + +``` +analyze the database schema +``` + +``` +build a dashboard showing products that are most frequently returned by our UK customers +``` + +**Save time with shortcuts** + +- Press `?` to see all available keyboard shortcuts +- Use Tab for command completion +- Press ↑ for command history +- Type `/` to see all slash commands + +## Getting help + +- **In Qwen Code**: Type `/help` or ask "how do I..." +- **Documentation**: You're here! Browse other guides +- **Community**: Join our [GitHub Discussion](https://github.com/QwenLM/qwen-code/discussions) for tips and support diff --git a/docs/users/reference/_meta.ts b/docs/users/reference/_meta.ts new file mode 100644 index 00000000..a4c232e8 --- /dev/null +++ b/docs/users/reference/_meta.ts @@ -0,0 +1,3 @@ +export default { + 'keyboard-shortcuts': 'Keyboard Shortcuts', +}; diff --git a/docs/cli/keyboard-shortcuts.md b/docs/users/reference/keyboard-shortcuts.md similarity index 100% rename from docs/cli/keyboard-shortcuts.md rename to docs/users/reference/keyboard-shortcuts.md diff --git a/docs/cli/Uninstall.md b/docs/users/support/Uninstall.md similarity index 86% rename from docs/cli/Uninstall.md rename to docs/users/support/Uninstall.md index bdc7ae65..f8970c88 100644 --- a/docs/cli/Uninstall.md +++ b/docs/users/support/Uninstall.md @@ -1,4 +1,4 @@ -# Uninstalling the CLI +# Uninstall Your uninstall method depends on how you ran the CLI. Follow the instructions for either npx or a global npm installation. @@ -33,7 +33,7 @@ Remove-Item -Path (Join-Path $env:LocalAppData "npm-cache\_npx") -Recurse -Force ## Method 2: Using npm (Global Install) -If you installed the CLI globally (e.g., `npm install -g @qwen-code/qwen-code`), use the `npm uninstall` command with the `-g` flag to remove it. +If you installed the CLI globally (e.g. `npm install -g @qwen-code/qwen-code`), use the `npm uninstall` command with the `-g` flag to remove it. ```bash npm uninstall -g @qwen-code/qwen-code diff --git a/docs/support/_meta.ts b/docs/users/support/_meta.ts similarity index 77% rename from docs/support/_meta.ts rename to docs/users/support/_meta.ts index 9140d4fe..1407565a 100644 --- a/docs/support/_meta.ts +++ b/docs/users/support/_meta.ts @@ -1,4 +1,6 @@ export default { troubleshooting: 'Troubleshooting', 'tos-privacy': 'Terms of Service', + + Uninstall: 'Uninstall', }; diff --git a/docs/support/tos-privacy.md b/docs/users/support/tos-privacy.md similarity index 82% rename from docs/support/tos-privacy.md rename to docs/users/support/tos-privacy.md index 046b1d08..84ec5c0a 100644 --- a/docs/support/tos-privacy.md +++ b/docs/users/support/tos-privacy.md @@ -23,19 +23,21 @@ When you authenticate using your qwen.ai account, these Terms of Service and Pri - **Terms of Service:** Your use is governed by the [Qwen Terms of Service](https://qwen.ai/termsservice). - **Privacy Notice:** The collection and use of your data is described in the [Qwen Privacy Policy](https://qwen.ai/privacypolicy). -For details about authentication setup, quotas, and supported features, see [Authentication Setup](./cli/authentication.md). +For details about authentication setup, quotas, and supported features, see [Authentication Setup](/users/configuration/settings). ## 2. If you are using OpenAI-Compatible API Authentication When you authenticate using API keys from OpenAI-compatible providers, the applicable Terms of Service and Privacy Notice depend on your chosen provider. -**Important:** When using OpenAI-compatible API authentication, you are subject to the terms and privacy policies of your chosen API provider, not Qwen Code's terms. Please review your provider's documentation for specific details about data usage, retention, and privacy practices. +> [!important] +> +> When using OpenAI-compatible API authentication, you are subject to the terms and privacy policies of your chosen API provider, not Qwen Code's terms. Please review your provider's documentation for specific details about data usage, retention, and privacy practices. Qwen Code supports various OpenAI-compatible providers. Please refer to your specific provider's terms of service and privacy policy for detailed information. ## Usage Statistics and Telemetry -Qwen Code may collect anonymous usage statistics and telemetry data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings. +Qwen Code may collect anonymous usage statistics and [telemetry](/developers/development/telemetry) data to improve the user experience and product quality. This data collection is optional and can be controlled through configuration settings. ### What Data is Collected @@ -50,10 +52,6 @@ When enabled, Qwen Code may collect: - **Qwen OAuth:** Usage statistics are governed by Qwen's privacy policy. You can opt-out through Qwen Code's configuration settings. - **OpenAI-Compatible API:** No additional data is collected by Qwen Code beyond what your chosen API provider collects. -### Opt-Out Instructions - -You can disable usage statistics collection by following the instructions in the [Usage Statistics Configuration](./cli/configuration.md#usage-statistics) documentation. - ## Frequently Asked Questions (FAQ) ### 1. Is my code, including prompts and answers, used to train AI models? @@ -85,8 +83,6 @@ When enabled, Qwen Code may collect: The Usage Statistics setting only controls data collection by Qwen Code itself. It does not affect what data your chosen AI service provider (Qwen, OpenAI, etc.) may collect according to their own privacy policies. -You can disable Usage Statistics collection by following the instructions in the [Usage Statistics Configuration](./cli/configuration.md#usage-statistics) documentation. - ### 3. How do I switch between authentication methods? You can switch between Qwen OAuth and OpenAI-compatible API authentication at any time: @@ -95,4 +91,4 @@ You can switch between Qwen OAuth and OpenAI-compatible API authentication at an 2. **Within the CLI**: Use the `/auth` command to reconfigure your authentication method 3. **Environment variables**: Set up `.env` files for automatic OpenAI-compatible API authentication -For detailed instructions, see the [Authentication Setup](./cli/authentication.md) documentation. +For detailed instructions, see the [Authentication Setup](/users/configuration/settings#environment-variables-for-api-access) documentation. diff --git a/docs/support/troubleshooting.md b/docs/users/support/troubleshooting.md similarity index 85% rename from docs/support/troubleshooting.md rename to docs/users/support/troubleshooting.md index f654c1f6..f5129300 100644 --- a/docs/support/troubleshooting.md +++ b/docs/users/support/troubleshooting.md @@ -1,4 +1,4 @@ -# Troubleshooting guide +# Troubleshooting This guide provides solutions to common issues and debugging tips, including topics on: @@ -31,7 +31,7 @@ This guide provides solutions to common issues and debugging tips, including top 1. In your home directory: `~/.qwen/settings.json`. 2. In your project's root directory: `./.qwen/settings.json`. - Refer to [Qwen Code Configuration](./cli/configuration.md) for more details. + Refer to [Qwen Code Configuration](/users/configuration/settings) for more details. - **Q: Why don't I see cached token counts in my stats output?** - A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Qwen API key or Google Cloud Vertex AI) but not for OAuth users (such as Google Personal/Enterprise accounts like Google Gmail or Google Workspace, respectively). This is because the Qwen Code Assist API does not support cached content creation. You can still view your total token usage using the `/stats` command. @@ -48,7 +48,7 @@ This guide provides solutions to common issues and debugging tips, including top - **Solution:** The update depends on how you installed Qwen Code: - If you installed `qwen` globally, check that your `npm` global binary directory is in your `PATH`. You can update using the command `npm install -g @qwen-code/qwen-code@latest`. - - If you are running `qwen` from source, ensure you are using the correct command to invoke it (e.g., `node packages/cli/dist/index.js ...`). To update, pull the latest changes from the repository, and then rebuild using the command `npm run build`. + - If you are running `qwen` from source, ensure you are using the correct command to invoke it (e.g. `node packages/cli/dist/index.js ...`). To update, pull the latest changes from the repository, and then rebuild using the command `npm run build`. - **Error: `MODULE_NOT_FOUND` or import errors.** - **Cause:** Dependencies are not installed correctly, or the project hasn't been built. @@ -59,12 +59,12 @@ This guide provides solutions to common issues and debugging tips, including top - **Error: "Operation not permitted", "Permission denied", or similar.** - **Cause:** When sandboxing is enabled, Qwen Code may attempt operations that are restricted by your sandbox configuration, such as writing outside the project directory or system temp directory. - - **Solution:** Refer to the [Configuration: Sandboxing](./cli/configuration.md#sandboxing) documentation for more information, including how to customize your sandbox configuration. + - **Solution:** Refer to the [Configuration: Sandboxing](/users/features/sandbox) documentation for more information, including how to customize your sandbox configuration. - **Qwen Code is not running in interactive mode in "CI" environments** - - **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g., `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment. + - **Issue:** Qwen Code does not enter interactive mode (no prompt appears) if an environment variable starting with `CI_` (e.g. `CI_TOKEN`) is set. This is because the `is-in-ci` package, used by the underlying UI framework, detects these variables and assumes a non-interactive CI environment. - **Cause:** The `is-in-ci` package checks for the presence of `CI`, `CONTINUOUS_INTEGRATION`, or any environment variable with a `CI_` prefix. When any of these are found, it signals that the environment is non-interactive, which prevents the CLI from starting in its interactive mode. - - **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g., `env -u CI_TOKEN qwen` + - **Solution:** If the `CI_` prefixed variable is not needed for the CLI to function, you can temporarily unset it for the command. e.g. `env -u CI_TOKEN qwen` - **DEBUG mode not working from project .env file** - **Issue:** Setting `DEBUG=true` in a project's `.env` file doesn't enable debug mode for the CLI. @@ -84,12 +84,12 @@ This guide provides solutions to common issues and debugging tips, including top The Qwen Code uses specific exit codes to indicate the reason for termination. This is especially useful for scripting and automation. -| Exit Code | Error Type | Description | -| --------- | -------------------------- | --------------------------------------------------------------------------------------------------- | -| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | -| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | -| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g., Docker, Podman, or Seatbelt). | -| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. | +| Exit Code | Error Type | Description | +| --------- | -------------------------- | ------------------------------------------------------------ | +| 41 | `FatalAuthenticationError` | An error occurred during the authentication process. | +| 42 | `FatalInputError` | Invalid or missing input was provided to the CLI. (non-interactive mode only) | +| 44 | `FatalSandboxError` | An error occurred with the sandboxing environment (e.g. Docker, Podman, or Seatbelt). | +| 52 | `FatalConfigError` | A configuration file (`settings.json`) is invalid or contains errors. | | 53 | `FatalTurnLimitedError` | The maximum number of conversational turns for the session was reached. (non-interactive mode only) | ## Debugging Tips @@ -101,7 +101,7 @@ The Qwen Code uses specific exit codes to indicate the reason for termination. T - **Core debugging:** - Check the server console output for error messages or stack traces. - Increase log verbosity if configurable. - - Use Node.js debugging tools (e.g., `node --inspect`) if you need to step through server-side code. + - Use Node.js debugging tools (e.g. `node --inspect`) if you need to step through server-side code. - **Tool issues:** - If a specific tool is failing, try to isolate the issue by running the simplest possible version of the command or operation the tool performs. diff --git a/eslint.config.js b/eslint.config.js index 5e5e1b85..26ec8edf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -263,4 +263,25 @@ export default tseslint.config( ], }, }, + // Settings for docs-site directory + { + files: ['docs-site/**/*.{js,jsx}'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + rules: { + // Allow relaxed rules for documentation site + '@typescript-eslint/no-unused-vars': 'off', + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'off', + }, + }, ); diff --git a/integration-tests/acp-integration.test.ts b/integration-tests/acp-integration.test.ts index b098e025..31e32da7 100644 --- a/integration-tests/acp-integration.test.ts +++ b/integration-tests/acp-integration.test.ts @@ -13,8 +13,6 @@ import { TestRig } from './test-helper.js'; const REQUEST_TIMEOUT_MS = 60_000; const INITIAL_PROMPT = 'Create a quick note (smoke test).'; -const RESUME_PROMPT = 'Continue the note after reload.'; -const LIST_SIZE = 5; const IS_SANDBOX = process.env['GEMINI_SANDBOX'] && process.env['GEMINI_SANDBOX']!.toLowerCase() !== 'false'; @@ -25,6 +23,14 @@ type PendingRequest = { timeout: NodeJS.Timeout; }; +type UsageMetadata = { + promptTokens?: number | null; + completionTokens?: number | null; + thoughtsTokens?: number | null; + totalTokens?: number | null; + cachedTokens?: number | null; +}; + type SessionUpdateNotification = { sessionId?: string; update?: { @@ -39,6 +45,9 @@ type SessionUpdateNotification = { text?: string; }; modeId?: string; + _meta?: { + usage?: UsageMetadata; + }; }; }; @@ -86,10 +95,14 @@ function setupAcpTest( const permissionHandler = options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' })); - const agent = spawn('node', [rig.bundlePath, '--experimental-acp'], { - cwd: rig.testDir!, - stdio: ['pipe', 'pipe', 'pipe'], - }); + const agent = spawn( + 'node', + [rig.bundlePath, '--experimental-acp', '--no-chat-recording'], + { + cwd: rig.testDir!, + stdio: ['pipe', 'pipe', 'pipe'], + }, + ); agent.stderr?.on('data', (chunk) => { stderr.push(chunk.toString()); @@ -253,11 +266,11 @@ function setupAcpTest( } (IS_SANDBOX ? describe.skip : describe)('acp integration', () => { - it('creates, lists, loads, and resumes a session', async () => { + it('basic smoke test', async () => { const rig = new TestRig(); rig.setup('acp load session'); - const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig); + const { sendRequest, cleanup, stderr } = setupAcpTest(rig); try { const initResult = await sendRequest('initialize', { @@ -283,34 +296,6 @@ function setupAcpTest( prompt: [{ type: 'text', text: INITIAL_PROMPT }], }); expect(promptResult).toBeDefined(); - - await delay(500); - - const listResult = (await sendRequest('session/list', { - cwd: rig.testDir!, - size: LIST_SIZE, - })) as { items?: Array<{ sessionId: string }> }; - - expect(Array.isArray(listResult.items)).toBe(true); - expect(listResult.items?.length ?? 0).toBeGreaterThan(0); - - const sessionToLoad = listResult.items![0].sessionId; - await sendRequest('session/load', { - cwd: rig.testDir!, - sessionId: sessionToLoad, - mcpServers: [], - }); - - const resumeResult = await sendRequest('session/prompt', { - sessionId: sessionToLoad, - prompt: [{ type: 'text', text: RESUME_PROMPT }], - }); - expect(resumeResult).toBeDefined(); - - const sessionsWithUpdates = sessionUpdates - .map((update) => update.sessionId) - .filter(Boolean); - expect(sessionsWithUpdates).toContain(sessionToLoad); } catch (e) { if (stderr.length) { console.error('Agent stderr:', stderr.join('')); @@ -587,4 +572,52 @@ function setupAcpTest( await cleanup(); } }); + + it('receives usage metadata in agent_message_chunk updates', async () => { + const rig = new TestRig(); + rig.setup('acp usage metadata'); + + const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig); + + try { + await sendRequest('initialize', { + protocolVersion: 1, + clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, + }); + await sendRequest('authenticate', { methodId: 'openai' }); + + const newSession = (await sendRequest('session/new', { + cwd: rig.testDir!, + mcpServers: [], + })) as { sessionId: string }; + + await sendRequest('session/prompt', { + sessionId: newSession.sessionId, + prompt: [{ type: 'text', text: 'Say "hello".' }], + }); + + await delay(500); + + // Find updates with usage metadata + const updatesWithUsage = sessionUpdates.filter( + (u) => + u.update?.sessionUpdate === 'agent_message_chunk' && + u.update?._meta?.usage, + ); + + expect(updatesWithUsage.length).toBeGreaterThan(0); + + const usage = updatesWithUsage[0].update?._meta?.usage; + expect(usage).toBeDefined(); + expect( + typeof usage?.promptTokens === 'number' || + typeof usage?.totalTokens === 'number', + ).toBe(true); + } catch (e) { + if (stderr.length) console.error('Agent stderr:', stderr.join('')); + throw e; + } finally { + await cleanup(); + } + }); }); diff --git a/integration-tests/sdk-typescript/configuration-options.test.ts b/integration-tests/sdk-typescript/configuration-options.test.ts index bc59cd79..ca218248 100644 --- a/integration-tests/sdk-typescript/configuration-options.test.ts +++ b/integration-tests/sdk-typescript/configuration-options.test.ts @@ -438,12 +438,8 @@ describe('Configuration Options (E2E)', () => { } }); - // Skip in containerized sandbox environments - qwen-oauth requires user interaction - // which is not possible in Docker/Podman CI environments - it.skipIf( - process.env['SANDBOX'] === 'sandbox:docker' || - process.env['SANDBOX'] === 'sandbox:podman', - )('should accept authType: qwen-oauth', async () => { + // Skip - qwen-oauth requires user interaction which is not possible in CI environments + it.skip('should accept authType: qwen-oauth', async () => { // Note: qwen-oauth requires credentials in ~/.qwen and user interaction // Without credentials, the auth process will timeout waiting for user // This test verifies the option is accepted and passed correctly to CLI diff --git a/integration-tests/sdk-typescript/test-helper.ts b/integration-tests/sdk-typescript/test-helper.ts index f3005655..d7efc026 100644 --- a/integration-tests/sdk-typescript/test-helper.ts +++ b/integration-tests/sdk-typescript/test-helper.ts @@ -73,15 +73,26 @@ export class SDKTestHelper { await mkdir(this.testDir, { recursive: true }); // Optionally create .qwen/settings.json for CLI configuration - if (options.createQwenConfig) { + if (options.createQwenConfig !== false) { const qwenDir = join(this.testDir, '.qwen'); await mkdir(qwenDir, { recursive: true }); + const optionsSettings = options.settings ?? {}; + const generalSettings = + typeof optionsSettings['general'] === 'object' && + optionsSettings['general'] !== null + ? (optionsSettings['general'] as Record) + : {}; + const settings = { + ...optionsSettings, telemetry: { enabled: false, // SDK tests don't need telemetry }, - ...options.settings, + general: { + ...generalSettings, + chatRecording: false, // SDK tests don't need chat recording + }, }; await writeFile( diff --git a/integration-tests/sdk-typescript/tool-control.test.ts b/integration-tests/sdk-typescript/tool-control.test.ts index b2b955a6..549f820c 100644 --- a/integration-tests/sdk-typescript/tool-control.test.ts +++ b/integration-tests/sdk-typescript/tool-control.test.ts @@ -31,9 +31,7 @@ describe('Tool Control Parameters (E2E)', () => { beforeEach(async () => { helper = new SDKTestHelper(); - testDir = await helper.setup('tool-control', { - createQwenConfig: false, - }); + testDir = await helper.setup('tool-control'); }); afterEach(async () => { diff --git a/integration-tests/test-helper.ts b/integration-tests/test-helper.ts index 0fe658c5..a08b3df5 100644 --- a/integration-tests/test-helper.ts +++ b/integration-tests/test-helper.ts @@ -218,8 +218,8 @@ export class TestRig { process.env.INTEGRATION_TEST_USE_INSTALLED_GEMINI === 'true'; const command = isNpmReleaseTest ? 'qwen' : 'node'; const initialArgs = isNpmReleaseTest - ? extraInitialArgs - : [this.bundlePath, ...extraInitialArgs]; + ? ['--no-chat-recording', ...extraInitialArgs] + : [this.bundlePath, '--no-chat-recording', ...extraInitialArgs]; return { command, initialArgs }; } diff --git a/package-lock.json b/package-lock.json index 14da548b..5526d051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@qwen-code/qwen-code", - "version": "0.4.1", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@qwen-code/qwen-code", - "version": "0.4.1", + "version": "0.5.1", "workspaces": [ "packages/*" ], @@ -17501,7 +17501,7 @@ }, "packages/cli": { "name": "@qwen-code/qwen-code", - "version": "0.4.1", + "version": "0.5.1", "dependencies": { "@google/genai": "1.16.0", "@iarna/toml": "^2.2.5", @@ -17616,7 +17616,7 @@ }, "packages/core": { "name": "@qwen-code/qwen-code-core", - "version": "0.4.1", + "version": "0.5.1", "hasInstallScript": true, "dependencies": { "@google/genai": "1.16.0", @@ -17757,7 +17757,7 @@ }, "packages/sdk-typescript": { "name": "@qwen-code/sdk", - "version": "0.1.0", + "version": "0.5.1", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4" @@ -18324,7 +18324,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -18746,7 +18745,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -19623,7 +19621,6 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -20189,7 +20186,7 @@ }, "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.4.1", + "version": "0.5.1", "dev": true, "license": "Apache-2.0", "devDependencies": { @@ -20201,7 +20198,7 @@ }, "packages/vscode-ide-companion": { "name": "qwen-code-vscode-ide-companion", - "version": "0.4.1", + "version": "0.5.1", "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.15.1", diff --git a/package.json b/package.json index 7b842b10..e5ed33bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code", - "version": "0.4.1", + "version": "0.5.1", "engines": { "node": ">=20.0.0" }, @@ -13,7 +13,7 @@ "url": "git+https://github.com/QwenLM/qwen-code.git" }, "config": { - "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1" }, "scripts": { "start": "cross-env node scripts/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index ab98e974..150c12e5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code", - "version": "0.4.1", + "version": "0.5.1", "description": "Qwen Code", "repository": { "type": "git", @@ -33,7 +33,7 @@ "dist" ], "config": { - "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.4.1" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.5.1" }, "dependencies": { "@google/genai": "1.16.0", diff --git a/packages/cli/src/acp-integration/acp.ts b/packages/cli/src/acp-integration/acp.ts index 2ef78bbd..84ba5ff5 100644 --- a/packages/cli/src/acp-integration/acp.ts +++ b/packages/cli/src/acp-integration/acp.ts @@ -88,6 +88,16 @@ export class AgentSideConnection implements Client { ); } + /** + * Streams authentication updates (e.g. Qwen OAuth authUri) to the client. + */ + async authenticateUpdate(params: schema.AuthenticateUpdate): Promise { + return await this.#connection.sendNotification( + schema.CLIENT_METHODS.authenticate_update, + params, + ); + } + /** * Request permission before running a tool * @@ -241,9 +251,11 @@ class Connection { ).toResult(); } + let errorName; let details; if (error instanceof Error) { + errorName = error.name; details = error.message; } else if ( typeof error === 'object' && @@ -254,6 +266,10 @@ class Connection { details = error.message; } + if (errorName === 'TokenManagerError') { + return RequestError.authRequired(details).toResult(); + } + return RequestError.internalError(details).toResult(); } } @@ -357,6 +373,7 @@ export interface Client { params: schema.RequestPermissionRequest, ): Promise; sessionUpdate(params: schema.SessionNotification): Promise; + authenticateUpdate(params: schema.AuthenticateUpdate): Promise; writeTextFile( params: schema.WriteTextFileRequest, ): Promise; diff --git a/packages/cli/src/acp-integration/acpAgent.ts b/packages/cli/src/acp-integration/acpAgent.ts index fc3c4ccc..91ce53cb 100644 --- a/packages/cli/src/acp-integration/acpAgent.ts +++ b/packages/cli/src/acp-integration/acpAgent.ts @@ -6,15 +6,19 @@ import type { ReadableStream, WritableStream } from 'node:stream/web'; -import type { Config, ConversationRecord } from '@qwen-code/qwen-code-core'; import { APPROVAL_MODE_INFO, APPROVAL_MODES, AuthType, clearCachedCredentialFile, + QwenOAuth2Event, + qwenOAuth2Events, MCPServerConfig, SessionService, buildApiHistoryFromConversation, + type Config, + type ConversationRecord, + type DeviceAuthorizationData, } from '@qwen-code/qwen-code-core'; import type { ApprovalModeValue } from './schema.js'; import * as acp from './acp.js'; @@ -123,13 +127,33 @@ class GeminiAgent { async authenticate({ methodId }: acp.AuthenticateRequest): Promise { const method = z.nativeEnum(AuthType).parse(methodId); + let authUri: string | undefined; + const authUriHandler = (deviceAuth: DeviceAuthorizationData) => { + authUri = deviceAuth.verification_uri_complete; + // Send the auth URL to ACP client as soon as it's available (refreshAuth is blocking). + void this.client.authenticateUpdate({ _meta: { authUri } }); + }; + + if (method === AuthType.QWEN_OAUTH) { + qwenOAuth2Events.once(QwenOAuth2Event.AuthUri, authUriHandler); + } + await clearCachedCredentialFile(); - await this.config.refreshAuth(method); - this.settings.setValue( - SettingScope.User, - 'security.auth.selectedType', - method, - ); + try { + await this.config.refreshAuth(method); + this.settings.setValue( + SettingScope.User, + 'security.auth.selectedType', + method, + ); + } finally { + // Ensure we don't leak listeners if auth fails early. + if (method === AuthType.QWEN_OAUTH) { + qwenOAuth2Events.off(QwenOAuth2Event.AuthUri, authUriHandler); + } + } + + return; } async newSession({ @@ -268,14 +292,17 @@ class GeminiAgent { private async ensureAuthenticated(config: Config): Promise { const selectedType = this.settings.merged.security?.auth?.selectedType; if (!selectedType) { - throw acp.RequestError.authRequired(); + throw acp.RequestError.authRequired('No Selected Type'); } try { - await config.refreshAuth(selectedType); + // Use true for the second argument to ensure only cached credentials are used + await config.refreshAuth(selectedType, true); } catch (e) { console.error(`Authentication failed: ${e}`); - throw acp.RequestError.authRequired(); + throw acp.RequestError.authRequired( + 'Authentication failed: ' + (e as Error).message, + ); } } diff --git a/packages/cli/src/acp-integration/schema.ts b/packages/cli/src/acp-integration/schema.ts index 8f21c74c..a557c519 100644 --- a/packages/cli/src/acp-integration/schema.ts +++ b/packages/cli/src/acp-integration/schema.ts @@ -20,6 +20,7 @@ export const AGENT_METHODS = { export const CLIENT_METHODS = { fs_read_text_file: 'fs/read_text_file', fs_write_text_file: 'fs/write_text_file', + authenticate_update: 'authenticate/update', session_request_permission: 'session/request_permission', session_update: 'session/update', }; @@ -57,8 +58,6 @@ export type CancelNotification = z.infer; export type AuthenticateRequest = z.infer; -export type AuthenticateResponse = z.infer; - export type NewSessionResponse = z.infer; export type LoadSessionResponse = z.infer; @@ -247,7 +246,13 @@ export const authenticateRequestSchema = z.object({ methodId: z.string(), }); -export const authenticateResponseSchema = z.null(); +export const authenticateUpdateSchema = z.object({ + _meta: z.object({ + authUri: z.string(), + }), +}); + +export type AuthenticateUpdate = z.infer; export const newSessionResponseSchema = z.object({ sessionId: z.string(), @@ -316,6 +321,23 @@ export const annotationsSchema = z.object({ priority: z.number().optional().nullable(), }); +export const usageSchema = z.object({ + promptTokens: z.number().optional().nullable(), + completionTokens: z.number().optional().nullable(), + thoughtsTokens: z.number().optional().nullable(), + totalTokens: z.number().optional().nullable(), + cachedTokens: z.number().optional().nullable(), +}); + +export type Usage = z.infer; + +export const sessionUpdateMetaSchema = z.object({ + usage: usageSchema.optional().nullable(), + durationMs: z.number().optional().nullable(), +}); + +export type SessionUpdateMeta = z.infer; + export const requestPermissionResponseSchema = z.object({ outcome: requestPermissionOutcomeSchema, }); @@ -500,10 +522,12 @@ export const sessionUpdateSchema = z.union([ z.object({ content: contentBlockSchema, sessionUpdate: z.literal('agent_message_chunk'), + _meta: sessionUpdateMetaSchema.optional().nullable(), }), z.object({ content: contentBlockSchema, sessionUpdate: z.literal('agent_thought_chunk'), + _meta: sessionUpdateMetaSchema.optional().nullable(), }), z.object({ content: z.array(toolCallContentSchema).optional(), @@ -536,7 +560,6 @@ export const sessionUpdateSchema = z.union([ export const agentResponseSchema = z.union([ initializeResponseSchema, - authenticateResponseSchema, newSessionResponseSchema, loadSessionResponseSchema, promptResponseSchema, diff --git a/packages/cli/src/acp-integration/service/filesystem.test.ts b/packages/cli/src/acp-integration/service/filesystem.test.ts new file mode 100644 index 00000000..70ccfc2d --- /dev/null +++ b/packages/cli/src/acp-integration/service/filesystem.test.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from 'vitest'; +import type { FileSystemService } from '@qwen-code/qwen-code-core'; +import { AcpFileSystemService } from './filesystem.js'; + +const createFallback = (): FileSystemService => ({ + readTextFile: vi.fn(), + writeTextFile: vi.fn(), + findFiles: vi.fn().mockReturnValue([]), +}); + +describe('AcpFileSystemService', () => { + describe('readTextFile ENOENT handling', () => { + it('parses path from ACP ENOENT message (quoted)', async () => { + const client = { + readTextFile: vi + .fn() + .mockResolvedValue({ content: 'ERROR: ENOENT: "/remote/file.txt"' }), + } as unknown as import('../acp.js').Client; + + const svc = new AcpFileSystemService( + client, + 'session-1', + { readTextFile: true, writeTextFile: true }, + createFallback(), + ); + + await expect(svc.readTextFile('/local/file.txt')).rejects.toMatchObject({ + code: 'ENOENT', + path: '/remote/file.txt', + }); + }); + + it('falls back to requested path when none provided', async () => { + const client = { + readTextFile: vi.fn().mockResolvedValue({ content: 'ERROR: ENOENT:' }), + } as unknown as import('../acp.js').Client; + + const svc = new AcpFileSystemService( + client, + 'session-2', + { readTextFile: true, writeTextFile: true }, + createFallback(), + ); + + await expect( + svc.readTextFile('/fallback/path.txt'), + ).rejects.toMatchObject({ + code: 'ENOENT', + path: '/fallback/path.txt', + }); + }); + }); +}); diff --git a/packages/cli/src/acp-integration/service/filesystem.ts b/packages/cli/src/acp-integration/service/filesystem.ts index c7db7235..7bcaee2d 100644 --- a/packages/cli/src/acp-integration/service/filesystem.ts +++ b/packages/cli/src/acp-integration/service/filesystem.ts @@ -30,6 +30,20 @@ export class AcpFileSystemService implements FileSystemService { limit: null, }); + if (response.content.startsWith('ERROR: ENOENT:')) { + // Treat ACP error strings as structured ENOENT errors without + // assuming a specific platform format. + const match = /^ERROR:\s*ENOENT:\s*(?.*)$/i.exec(response.content); + const err = new Error(response.content) as NodeJS.ErrnoException; + err.code = 'ENOENT'; + err.errno = -2; + const rawPath = match?.groups?.['path']?.trim(); + err['path'] = rawPath + ? rawPath.replace(/^['"]|['"]$/g, '') || filePath + : filePath; + throw err; + } + return response.content; } diff --git a/packages/cli/src/acp-integration/session/HistoryReplayer.test.ts b/packages/cli/src/acp-integration/session/HistoryReplayer.test.ts index 83451592..c9cf65fb 100644 --- a/packages/cli/src/acp-integration/session/HistoryReplayer.test.ts +++ b/packages/cli/src/acp-integration/session/HistoryReplayer.test.ts @@ -411,4 +411,48 @@ describe('HistoryReplayer', () => { ]); }); }); + + describe('usage metadata replay', () => { + it('should emit usage metadata after assistant message content', async () => { + const record: ChatRecord = { + uuid: 'assistant-uuid', + parentUuid: 'user-uuid', + sessionId: 'test-session', + timestamp: new Date().toISOString(), + type: 'assistant', + cwd: '/test', + version: '1.0.0', + message: { + role: 'model', + parts: [{ text: 'Hello!' }], + }, + usageMetadata: { + promptTokenCount: 100, + candidatesTokenCount: 50, + totalTokenCount: 150, + }, + }; + + await replayer.replay([record]); + + expect(sendUpdateSpy).toHaveBeenCalledTimes(2); + expect(sendUpdateSpy).toHaveBeenNthCalledWith(1, { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'Hello!' }, + }); + expect(sendUpdateSpy).toHaveBeenNthCalledWith(2, { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: '' }, + _meta: { + usage: { + promptTokens: 100, + completionTokens: 50, + thoughtsTokens: undefined, + totalTokens: 150, + cachedTokens: undefined, + }, + }, + }); + }); + }); }); diff --git a/packages/cli/src/acp-integration/session/HistoryReplayer.ts b/packages/cli/src/acp-integration/session/HistoryReplayer.ts index 53a1ed8a..0ecbccb9 100644 --- a/packages/cli/src/acp-integration/session/HistoryReplayer.ts +++ b/packages/cli/src/acp-integration/session/HistoryReplayer.ts @@ -4,8 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { ChatRecord } from '@qwen-code/qwen-code-core'; -import type { Content } from '@google/genai'; +import type { ChatRecord, TaskResultDisplay } from '@qwen-code/qwen-code-core'; +import type { + Content, + GenerateContentResponseUsageMetadata, +} from '@google/genai'; import type { SessionContext } from './types.js'; import { MessageEmitter } from './emitters/MessageEmitter.js'; import { ToolCallEmitter } from './emitters/ToolCallEmitter.js'; @@ -52,6 +55,9 @@ export class HistoryReplayer { if (record.message) { await this.replayContent(record.message, 'assistant'); } + if (record.usageMetadata) { + await this.replayUsageMetadata(record.usageMetadata); + } break; case 'tool_result': @@ -88,11 +94,22 @@ export class HistoryReplayer { toolName: functionName, callId, args: part.functionCall.args as Record, + status: 'in_progress', }); } } } + /** + * Replays usage metadata. + * @param usageMetadata - The usage metadata to replay + */ + private async replayUsageMetadata( + usageMetadata: GenerateContentResponseUsageMetadata, + ): Promise { + await this.messageEmitter.emitUsageMetadata(usageMetadata); + } + /** * Replays a tool result record. */ @@ -118,6 +135,54 @@ export class HistoryReplayer { // Note: args aren't stored in tool_result records by default args: undefined, }); + + // Special handling: Task tool execution summary contains token usage + const { resultDisplay } = result ?? {}; + if ( + !!resultDisplay && + typeof resultDisplay === 'object' && + 'type' in resultDisplay && + (resultDisplay as { type?: unknown }).type === 'task_execution' + ) { + await this.emitTaskUsageFromResultDisplay( + resultDisplay as TaskResultDisplay, + ); + } + } + + /** + * Emits token usage from a TaskResultDisplay execution summary, if present. + */ + private async emitTaskUsageFromResultDisplay( + resultDisplay: TaskResultDisplay, + ): Promise { + const summary = resultDisplay.executionSummary; + if (!summary) { + return; + } + + const usageMetadata: GenerateContentResponseUsageMetadata = {}; + + if (Number.isFinite(summary.inputTokens)) { + usageMetadata.promptTokenCount = summary.inputTokens; + } + if (Number.isFinite(summary.outputTokens)) { + usageMetadata.candidatesTokenCount = summary.outputTokens; + } + if (Number.isFinite(summary.thoughtTokens)) { + usageMetadata.thoughtsTokenCount = summary.thoughtTokens; + } + if (Number.isFinite(summary.cachedTokens)) { + usageMetadata.cachedContentTokenCount = summary.cachedTokens; + } + if (Number.isFinite(summary.totalTokens)) { + usageMetadata.totalTokenCount = summary.totalTokens; + } + + // Only emit if we captured at least one token metric + if (Object.keys(usageMetadata).length > 0) { + await this.messageEmitter.emitUsageMetadata(usageMetadata); + } } /** diff --git a/packages/cli/src/acp-integration/session/Session.ts b/packages/cli/src/acp-integration/session/Session.ts index b4d79433..1d90ed20 100644 --- a/packages/cli/src/acp-integration/session/Session.ts +++ b/packages/cli/src/acp-integration/session/Session.ts @@ -4,7 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Content, FunctionCall, Part } from '@google/genai'; +import type { + Content, + FunctionCall, + GenerateContentResponseUsageMetadata, + Part, +} from '@google/genai'; import type { Config, GeminiChat, @@ -55,6 +60,7 @@ import type { SessionContext, ToolCallStartParams } from './types.js'; import { HistoryReplayer } from './HistoryReplayer.js'; import { ToolCallEmitter } from './emitters/ToolCallEmitter.js'; import { PlanEmitter } from './emitters/PlanEmitter.js'; +import { MessageEmitter } from './emitters/MessageEmitter.js'; import { SubAgentTracker } from './SubAgentTracker.js'; /** @@ -79,6 +85,7 @@ export class Session implements SessionContext { private readonly historyReplayer: HistoryReplayer; private readonly toolCallEmitter: ToolCallEmitter; private readonly planEmitter: PlanEmitter; + private readonly messageEmitter: MessageEmitter; // Implement SessionContext interface readonly sessionId: string; @@ -96,6 +103,7 @@ export class Session implements SessionContext { this.toolCallEmitter = new ToolCallEmitter(this); this.planEmitter = new PlanEmitter(this); this.historyReplayer = new HistoryReplayer(this); + this.messageEmitter = new MessageEmitter(this); } getId(): string { @@ -192,6 +200,8 @@ export class Session implements SessionContext { } const functionCalls: FunctionCall[] = []; + let usageMetadata: GenerateContentResponseUsageMetadata | null = null; + const streamStartTime = Date.now(); try { const responseStream = await chat.sendMessageStream( @@ -222,20 +232,18 @@ export class Session implements SessionContext { continue; } - const content: acp.ContentBlock = { - type: 'text', - text: part.text, - }; - - this.sendUpdate({ - sessionUpdate: part.thought - ? 'agent_thought_chunk' - : 'agent_message_chunk', - content, - }); + this.messageEmitter.emitMessage( + part.text, + 'assistant', + part.thought, + ); } } + if (resp.type === StreamEventType.CHUNK && resp.value.usageMetadata) { + usageMetadata = resp.value.usageMetadata; + } + if (resp.type === StreamEventType.CHUNK && resp.value.functionCalls) { functionCalls.push(...resp.value.functionCalls); } @@ -251,6 +259,15 @@ export class Session implements SessionContext { throw error; } + if (usageMetadata) { + const durationMs = Date.now() - streamStartTime; + await this.messageEmitter.emitUsageMetadata( + usageMetadata, + '', + durationMs, + ); + } + if (functionCalls.length > 0) { const toolResponseParts: Part[] = []; @@ -444,7 +461,9 @@ export class Session implements SessionContext { } const confirmationDetails = - await invocation.shouldConfirmExecute(abortSignal); + this.config.getApprovalMode() !== ApprovalMode.YOLO + ? await invocation.shouldConfirmExecute(abortSignal) + : false; if (confirmationDetails) { const content: acp.ToolCallContent[] = []; @@ -522,6 +541,7 @@ export class Session implements SessionContext { callId, toolName: fc.name, args, + status: 'in_progress', }; await this.toolCallEmitter.emitStart(startParams); } diff --git a/packages/cli/src/acp-integration/session/SubAgentTracker.test.ts b/packages/cli/src/acp-integration/session/SubAgentTracker.test.ts index 074c8162..f2bb7cc5 100644 --- a/packages/cli/src/acp-integration/session/SubAgentTracker.test.ts +++ b/packages/cli/src/acp-integration/session/SubAgentTracker.test.ts @@ -208,7 +208,7 @@ describe('SubAgentTracker', () => { expect.objectContaining({ sessionUpdate: 'tool_call', toolCallId: 'call-123', - status: 'in_progress', + status: 'pending', title: 'read_file', content: [], locations: [], diff --git a/packages/cli/src/acp-integration/session/SubAgentTracker.ts b/packages/cli/src/acp-integration/session/SubAgentTracker.ts index c6c83292..1e745b92 100644 --- a/packages/cli/src/acp-integration/session/SubAgentTracker.ts +++ b/packages/cli/src/acp-integration/session/SubAgentTracker.ts @@ -9,6 +9,7 @@ import type { SubAgentToolCallEvent, SubAgentToolResultEvent, SubAgentApprovalRequestEvent, + SubAgentUsageEvent, ToolCallConfirmationDetails, AnyDeclarativeTool, AnyToolInvocation, @@ -20,6 +21,7 @@ import { import { z } from 'zod'; import type { SessionContext } from './types.js'; import { ToolCallEmitter } from './emitters/ToolCallEmitter.js'; +import { MessageEmitter } from './emitters/MessageEmitter.js'; import type * as acp from '../acp.js'; /** @@ -62,6 +64,7 @@ const basicPermissionOptions: readonly PermissionOptionConfig[] = [ */ export class SubAgentTracker { private readonly toolCallEmitter: ToolCallEmitter; + private readonly messageEmitter: MessageEmitter; private readonly toolStates = new Map< string, { @@ -76,6 +79,7 @@ export class SubAgentTracker { private readonly client: acp.Client, ) { this.toolCallEmitter = new ToolCallEmitter(ctx); + this.messageEmitter = new MessageEmitter(ctx); } /** @@ -92,16 +96,19 @@ export class SubAgentTracker { const onToolCall = this.createToolCallHandler(abortSignal); const onToolResult = this.createToolResultHandler(abortSignal); const onApproval = this.createApprovalHandler(abortSignal); + const onUsageMetadata = this.createUsageMetadataHandler(abortSignal); eventEmitter.on(SubAgentEventType.TOOL_CALL, onToolCall); eventEmitter.on(SubAgentEventType.TOOL_RESULT, onToolResult); eventEmitter.on(SubAgentEventType.TOOL_WAITING_APPROVAL, onApproval); + eventEmitter.on(SubAgentEventType.USAGE_METADATA, onUsageMetadata); return [ () => { eventEmitter.off(SubAgentEventType.TOOL_CALL, onToolCall); eventEmitter.off(SubAgentEventType.TOOL_RESULT, onToolResult); eventEmitter.off(SubAgentEventType.TOOL_WAITING_APPROVAL, onApproval); + eventEmitter.off(SubAgentEventType.USAGE_METADATA, onUsageMetadata); // Clean up any remaining states this.toolStates.clear(); }, @@ -252,6 +259,20 @@ export class SubAgentTracker { }; } + /** + * Creates a handler for usage metadata events. + */ + private createUsageMetadataHandler( + abortSignal: AbortSignal, + ): (...args: unknown[]) => void { + return (...args: unknown[]) => { + const event = args[0] as SubAgentUsageEvent; + if (abortSignal.aborted) return; + + this.messageEmitter.emitUsageMetadata(event.usage, '', event.durationMs); + }; + } + /** * Converts confirmation details to permission options for the client. */ diff --git a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts index 52a41a48..d0b1ae87 100644 --- a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts +++ b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.test.ts @@ -148,4 +148,59 @@ describe('MessageEmitter', () => { }); }); }); + + describe('emitUsageMetadata', () => { + it('should emit agent_message_chunk with _meta.usage containing token counts', async () => { + const usageMetadata = { + promptTokenCount: 100, + candidatesTokenCount: 50, + thoughtsTokenCount: 25, + totalTokenCount: 175, + cachedContentTokenCount: 10, + }; + + await emitter.emitUsageMetadata(usageMetadata); + + expect(sendUpdateSpy).toHaveBeenCalledWith({ + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: '' }, + _meta: { + usage: { + promptTokens: 100, + completionTokens: 50, + thoughtsTokens: 25, + totalTokens: 175, + cachedTokens: 10, + }, + }, + }); + }); + + it('should include durationMs in _meta when provided', async () => { + const usageMetadata = { + promptTokenCount: 10, + candidatesTokenCount: 5, + thoughtsTokenCount: 2, + totalTokenCount: 17, + cachedContentTokenCount: 1, + }; + + await emitter.emitUsageMetadata(usageMetadata, 'done', 1234); + + expect(sendUpdateSpy).toHaveBeenCalledWith({ + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'done' }, + _meta: { + usage: { + promptTokens: 10, + completionTokens: 5, + thoughtsTokens: 2, + totalTokens: 17, + cachedTokens: 1, + }, + durationMs: 1234, + }, + }); + }); + }); }); diff --git a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts index 9ac8943a..39cdf6a7 100644 --- a/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts +++ b/packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { GenerateContentResponseUsageMetadata } from '@google/genai'; +import type { Usage } from '../../schema.js'; import { BaseEmitter } from './BaseEmitter.js'; /** @@ -24,6 +26,16 @@ export class MessageEmitter extends BaseEmitter { }); } + /** + * Emits an agent thought chunk. + */ + async emitAgentThought(text: string): Promise { + await this.sendUpdate({ + sessionUpdate: 'agent_thought_chunk', + content: { type: 'text', text }, + }); + } + /** * Emits an agent message chunk. */ @@ -35,12 +47,28 @@ export class MessageEmitter extends BaseEmitter { } /** - * Emits an agent thought chunk. + * Emits usage metadata. */ - async emitAgentThought(text: string): Promise { + async emitUsageMetadata( + usageMetadata: GenerateContentResponseUsageMetadata, + text: string = '', + durationMs?: number, + ): Promise { + const usage: Usage = { + promptTokens: usageMetadata.promptTokenCount, + completionTokens: usageMetadata.candidatesTokenCount, + thoughtsTokens: usageMetadata.thoughtsTokenCount, + totalTokens: usageMetadata.totalTokenCount, + cachedTokens: usageMetadata.cachedContentTokenCount, + }; + + const meta = + typeof durationMs === 'number' ? { usage, durationMs } : { usage }; + await this.sendUpdate({ - sessionUpdate: 'agent_thought_chunk', + sessionUpdate: 'agent_message_chunk', content: { type: 'text', text }, + _meta: meta, }); } diff --git a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts index 52e13399..4616b859 100644 --- a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts +++ b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.test.ts @@ -71,7 +71,7 @@ describe('ToolCallEmitter', () => { expect(sendUpdateSpy).toHaveBeenCalledWith({ sessionUpdate: 'tool_call', toolCallId: 'call-123', - status: 'in_progress', + status: 'pending', title: 'unknown_tool', // Falls back to tool name content: [], locations: [], @@ -94,7 +94,7 @@ describe('ToolCallEmitter', () => { expect(sendUpdateSpy).toHaveBeenCalledWith({ sessionUpdate: 'tool_call', toolCallId: 'call-456', - status: 'in_progress', + status: 'pending', title: 'edit_file: Test tool description', content: [], locations: [{ path: '/test/file.ts', line: 10 }], @@ -144,7 +144,7 @@ describe('ToolCallEmitter', () => { expect(sendUpdateSpy).toHaveBeenCalledWith({ sessionUpdate: 'tool_call', toolCallId: 'call-fail', - status: 'in_progress', + status: 'pending', title: 'failing_tool', // Fallback to tool name content: [], locations: [], // Fallback to empty @@ -493,7 +493,7 @@ describe('ToolCallEmitter', () => { type: 'content', content: { type: 'text', - text: '{"output":"test output"}', + text: 'test output', }, }, ], @@ -650,7 +650,7 @@ describe('ToolCallEmitter', () => { content: [ { type: 'content', - content: { type: 'text', text: '{"output":"Function output"}' }, + content: { type: 'text', text: 'Function output' }, }, ], rawOutput: 'raw result', diff --git a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts index 4c25570a..9859ed78 100644 --- a/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts +++ b/packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts @@ -59,7 +59,7 @@ export class ToolCallEmitter extends BaseEmitter { await this.sendUpdate({ sessionUpdate: 'tool_call', toolCallId: params.callId, - status: 'in_progress', + status: params.status || 'pending', title, content: [], locations, @@ -275,7 +275,18 @@ export class ToolCallEmitter extends BaseEmitter { // Handle functionResponse parts - stringify the response if ('functionResponse' in part && part.functionResponse) { try { - const responseText = JSON.stringify(part.functionResponse.response); + const resp = part.functionResponse.response as Record< + string, + unknown + >; + const outputField = resp['output']; + const errorField = resp['error']; + const responseText = + typeof outputField === 'string' + ? outputField + : typeof errorField === 'string' + ? errorField + : JSON.stringify(resp); result.push({ type: 'content', content: { type: 'text', text: responseText }, diff --git a/packages/cli/src/acp-integration/session/types.ts b/packages/cli/src/acp-integration/session/types.ts index 0c8f60a0..7812fb03 100644 --- a/packages/cli/src/acp-integration/session/types.ts +++ b/packages/cli/src/acp-integration/session/types.ts @@ -35,6 +35,8 @@ export interface ToolCallStartParams { callId: string; /** Arguments passed to the tool */ args?: Record; + /** Status of the tool call */ + status?: 'pending' | 'in_progress' | 'completed' | 'failed'; } /** diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 3212996d..99d0c0ed 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -130,6 +130,11 @@ export interface CliArgs { inputFormat?: string | undefined; outputFormat: string | undefined; includePartialMessages?: boolean; + /** + * If chat recording is disabled, the chat history would not be recorded, + * so --continue and --resume would not take effect. + */ + chatRecording: boolean | undefined; /** Resume the most recent session for the current project */ continue: boolean | undefined; /** Resume a specific session by its ID */ @@ -138,6 +143,7 @@ export interface CliArgs { coreTools: string[] | undefined; excludeTools: string[] | undefined; authType: string | undefined; + channel: string | undefined; } function normalizeOutputFormat( @@ -232,6 +238,11 @@ export async function parseArguments(settings: Settings): Promise { 'proxy', 'Use the "proxy" setting in settings.json instead. This flag will be removed in a future version.', ) + .option('chat-recording', { + type: 'boolean', + description: + 'Enable chat recording to disk. If false, chat history is not saved and --continue/--resume will not work.', + }) .command('$0 [query..]', 'Launch Qwen Code CLI', (yargsInstance: Argv) => yargsInstance .positional('query', { @@ -297,6 +308,11 @@ export async function parseArguments(settings: Settings): Promise { type: 'boolean', description: 'Starts the agent in ACP mode', }) + .option('channel', { + type: 'string', + choices: ['VSCode', 'ACP', 'SDK', 'CI'], + description: 'Channel identifier (VSCode, ACP, SDK, CI)', + }) .option('allowed-mcp-server-names', { type: 'array', string: true, @@ -559,6 +575,12 @@ export async function parseArguments(settings: Settings): Promise { // The import format is now only controlled by settings.memoryImportFormat // We no longer accept it as a CLI argument + + // Apply ACP fallback: if experimental-acp is present but no explicit --channel, treat as ACP + if (result['experimentalAcp'] && !result['channel']) { + (result as Record)['channel'] = 'ACP'; + } + return result as unknown as CliArgs; } @@ -983,6 +1005,12 @@ export async function loadCliConfig( output: { format: outputSettingsFormat, }, + channel: argv.channel, + // Precedence: explicit CLI flag > settings file > default(true). + // NOTE: do NOT set a yargs default for `chat-recording`, otherwise argv will + // always be true and the settings file can never disable recording. + chatRecording: + argv.chatRecording ?? settings.general?.chatRecording ?? true, }); } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 340cab77..6f272ea8 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -191,8 +191,29 @@ const SETTINGS_SCHEMA = { { value: 'auto', label: 'Auto (detect from system)' }, { value: 'en', label: 'English' }, { value: 'zh', label: '中文 (Chinese)' }, + { value: 'ru', label: 'Русский (Russian)' }, ], }, + terminalBell: { + type: 'boolean', + label: 'Terminal Bell', + category: 'General', + requiresRestart: false, + default: true, + description: + 'Play terminal bell sound when response completes or needs approval.', + showInDialog: true, + }, + chatRecording: { + type: 'boolean', + label: 'Chat Recording', + category: 'General', + requiresRestart: true, + default: true, + description: + 'Enable saving chat history to disk. Disabling this will also prevent --continue and --resume from working.', + showInDialog: false, + }, }, }, output: { diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index f602d17d..7bb78aaf 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -485,6 +485,8 @@ describe('gemini.tsx main function kitty protocol', () => { excludeTools: undefined, authType: undefined, maxSessionTurns: undefined, + channel: undefined, + chatRecording: undefined, }); await main(); diff --git a/packages/cli/src/i18n/index.ts b/packages/cli/src/i18n/index.ts index 2cad8dec..7436336b 100644 --- a/packages/cli/src/i18n/index.ts +++ b/packages/cli/src/i18n/index.ts @@ -9,7 +9,7 @@ import * as path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { homedir } from 'node:os'; -export type SupportedLanguage = 'en' | 'zh' | string; // Allow custom language codes +export type SupportedLanguage = 'en' | 'zh' | 'ru' | string; // Allow custom language codes // State let currentLanguage: SupportedLanguage = 'en'; @@ -51,10 +51,12 @@ export function detectSystemLanguage(): SupportedLanguage { const envLang = process.env['QWEN_CODE_LANG'] || process.env['LANG']; if (envLang?.startsWith('zh')) return 'zh'; if (envLang?.startsWith('en')) return 'en'; + if (envLang?.startsWith('ru')) return 'ru'; try { const locale = Intl.DateTimeFormat().resolvedOptions().locale; if (locale.startsWith('zh')) return 'zh'; + if (locale.startsWith('ru')) return 'ru'; } catch { // Fallback to default } diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index ddf1d4a7..4e8b36cc 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -868,6 +868,7 @@ export default { // Exit Screen / Stats // ============================================================================ 'Agent powering down. Goodbye!': 'Agent powering down. Goodbye!', + 'To continue this session, run': 'To continue this session, run', 'Interaction Summary': 'Interaction Summary', 'Session ID:': 'Session ID:', 'Tool Calls:': 'Tool Calls:', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js new file mode 100644 index 00000000..009578be --- /dev/null +++ b/packages/cli/src/i18n/locales/ru.js @@ -0,0 +1,1121 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +// Русский перевод для Qwen Code CLI +// Ключ служит одновременно ключом перевода и текстом по умолчанию + +export default { + // ============================================================================ + // Справка / Компоненты интерфейса + // ============================================================================ + 'Basics:': 'Основы:', + 'Add context': 'Добавить контекст', + 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.': + 'Используйте {{symbol}} для добавления файлов в контекст (например, {{example}}) для выбора конкретных файлов или папок).', + '@': '@', + '@src/myFile.ts': '@src/myFile.ts', + 'Shell mode': 'Режим терминала', + 'YOLO mode': 'Режим YOLO', + 'plan mode': 'Режим планирования', + 'auto-accept edits': 'Режим принятия правок', + 'Accepting edits': 'Принятие правок', + '(shift + tab to cycle)': '(shift + tab для переключения)', + 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': + 'Выполняйте команды терминала через {{symbol}} (например, {{example1}}) или используйте естественный язык (например, {{example2}}).', + '!': '!', + '!npm run start': '!npm run start', + 'start server': 'start server', + 'Commands:': 'Команды:', + 'shell command': 'команда терминала', + 'Model Context Protocol command (from external servers)': + 'Команда Model Context Protocol (из внешних серверов)', + 'Keyboard Shortcuts:': 'Горячие клавиши:', + 'Jump through words in the input': 'Переход по словам во вводе', + 'Close dialogs, cancel requests, or quit application': + 'Закрыть диалоги, отменить запросы или выйти из приложения', + 'New line': 'Новая строка', + 'New line (Alt+Enter works for certain linux distros)': + 'Новая строка (Alt+Enter работает только в некоторых дистрибутивах Linux)', + 'Clear the screen': 'Очистить экран', + 'Open input in external editor': 'Открыть ввод во внешнем редакторе', + 'Send message': 'Отправить сообщение', + 'Initializing...': 'Инициализация...', + 'Connecting to MCP servers... ({{connected}}/{{total}})': + 'Подключение к MCP-серверам... ({{connected}}/{{total}})', + 'Type your message or @path/to/file': 'Введите сообщение или @путь/к/файлу', + "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.": + "Нажмите 'i' для режима ВСТАВКА и 'Esc' для ОБЫЧНОГО режима.", + 'Cancel operation / Clear input (double press)': + 'Отменить операцию / Очистить ввод (двойное нажатие)', + 'Cycle approval modes': 'Переключение режимов подтверждения', + 'Cycle through your prompt history': 'Пролистать историю запросов', + 'For a full list of shortcuts, see {{docPath}}': + 'Полный список горячих клавиш см. в {{docPath}}', + 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md', + 'for help on Qwen Code': 'Справка по Qwen Code', + 'show version info': 'Просмотр информации о версии', + 'submit a bug report': 'Отправка отчёта об ошибке', + 'About Qwen Code': 'Об Qwen Code', + + // ============================================================================ + // Поля системной информации + // ============================================================================ + 'CLI Version': 'Версия CLI', + 'Git Commit': 'Git-коммит', + Model: 'Модель', + Sandbox: 'Песочница', + 'OS Platform': 'Платформа ОС', + 'OS Arch': 'Архитектура ОС', + 'OS Release': 'Версия ОС', + 'Node.js Version': 'Версия Node.js', + 'NPM Version': 'Версия NPM', + 'Session ID': 'ID сессии', + 'Auth Method': 'Метод авторизации', + 'Base URL': 'Базовый URL', + 'Memory Usage': 'Использование памяти', + 'IDE Client': 'Клиент IDE', + + // ============================================================================ + // Команды - Общие + // ============================================================================ + 'Analyzes the project and creates a tailored QWEN.md file.': + 'Анализ проекта и создание адаптированного файла QWEN.md', + 'list available Qwen Code tools. Usage: /tools [desc]': + 'Просмотр доступных инструментов Qwen Code. Использование: /tools [desc]', + 'Available Qwen Code CLI tools:': 'Доступные инструменты Qwen Code CLI:', + 'No tools available': 'Нет доступных инструментов', + 'View or change the approval mode for tool usage': + 'Просмотр или изменение режима подтверждения для использования инструментов', + 'View or change the language setting': + 'Просмотр или изменение настроек языка', + 'change the theme': 'Изменение темы', + 'Select Theme': 'Выбор темы', + Preview: 'Предпросмотр', + '(Use Enter to select, Tab to configure scope)': + '(Enter для выбора, Tab для настройки области)', + '(Use Enter to apply scope, Tab to select theme)': + '(Enter для применения области, Tab для выбора темы)', + 'Theme configuration unavailable due to NO_COLOR env variable.': + 'Настройка темы недоступна из-за переменной окружения NO_COLOR.', + 'Theme "{{themeName}}" not found.': 'Тема "{{themeName}}" не найдена.', + 'Theme "{{themeName}}" not found in selected scope.': + 'Тема "{{themeName}}" не найдена в выбранной области.', + 'clear the screen and conversation history': + 'Очистка экрана и истории диалога', + 'Compresses the context by replacing it with a summary.': + 'Сжатие контекста заменой на краткую сводку', + 'open full Qwen Code documentation in your browser': + 'Открытие полной документации Qwen Code в браузере', + 'Configuration not available.': 'Конфигурация недоступна.', + 'change the auth method': 'Изменение метода авторизации', + 'Copy the last result or code snippet to clipboard': + 'Копирование последнего результата или фрагмента кода в буфер обмена', + + // ============================================================================ + // Команды - Агенты + // ============================================================================ + 'Manage subagents for specialized task delegation.': + 'Управление подагентами для делегирования специализированных задач', + 'Manage existing subagents (view, edit, delete).': + 'Управление существующими подагентами (просмотр, правка, удаление)', + 'Create a new subagent with guided setup.': + 'Создание нового подагента с пошаговой настройкой', + + // ============================================================================ + // Агенты - Диалог управления + // ============================================================================ + Agents: 'Агенты', + 'Choose Action': 'Выберите действие', + 'Edit {{name}}': 'Редактировать {{name}}', + 'Edit Tools: {{name}}': 'Редактировать инструменты: {{name}}', + 'Edit Color: {{name}}': 'Редактировать цвет: {{name}}', + 'Delete {{name}}': 'Удалить {{name}}', + 'Unknown Step': 'Неизвестный шаг', + 'Esc to close': 'Esc для закрытия', + 'Enter to select, ↑↓ to navigate, Esc to close': + 'Enter для выбора, ↑↓ для навигации, Esc для закрытия', + 'Esc to go back': 'Esc для возврата', + 'Enter to confirm, Esc to cancel': 'Enter для подтверждения, Esc для отмены', + 'Enter to select, ↑↓ to navigate, Esc to go back': + 'Enter для выбора, ↑↓ для навигации, Esc для возврата', + 'Invalid step: {{step}}': 'Неверный шаг: {{step}}', + 'No subagents found.': 'Подагенты не найдены.', + "Use '/agents create' to create your first subagent.": + "Используйте '/agents create' для создания первого подагента.", + '(built-in)': '(встроенный)', + '(overridden by project level agent)': + '(переопределен агентом уровня проекта)', + 'Project Level ({{path}})': 'Уровень проекта ({{path}})', + 'User Level ({{path}})': 'Уровень пользователя ({{path}})', + 'Built-in Agents': 'Встроенные агенты', + 'Using: {{count}} agents': 'Используется: {{count}} агент(ов)', + 'View Agent': 'Просмотреть агента', + 'Edit Agent': 'Редактировать агента', + 'Delete Agent': 'Удалить агента', + Back: 'Назад', + 'No agent selected': 'Агент не выбран', + 'File Path: ': 'Путь к файлу: ', + 'Tools: ': 'Инструменты: ', + 'Color: ': 'Цвет: ', + 'Description:': 'Описание:', + 'System Prompt:': 'Системный промпт:', + 'Open in editor': 'Открыть в редакторе', + 'Edit tools': 'Редактировать инструменты', + 'Edit color': 'Редактировать цвет', + '❌ Error:': '❌ Ошибка:', + 'Are you sure you want to delete agent "{{name}}"?': + 'Вы уверены, что хотите удалить агента "{{name}}"?', + // ============================================================================ + // Агенты - Мастер создания + // ============================================================================ + 'Project Level (.qwen/agents/)': 'Уровень проекта (.qwen/agents/)', + 'User Level (~/.qwen/agents/)': 'Уровень пользователя (~/.qwen/agents/)', + '✅ Subagent Created Successfully!': '✅ Подагент успешно создан!', + 'Subagent "{{name}}" has been saved to {{level}} level.': + 'Подагент "{{name}}" сохранен на уровне {{level}}.', + 'Name: ': 'Имя: ', + 'Location: ': 'Расположение: ', + '❌ Error saving subagent:': '❌ Ошибка сохранения подагента:', + 'Warnings:': 'Предупреждения:', + 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent': + 'Имя "{{name}}" уже существует на уровне {{level}} - существующий подагент будет перезаписан', + 'Name "{{name}}" exists at user level - project level will take precedence': + 'Имя "{{name}}" существует на уровне пользователя - уровень проекта будет иметь приоритет', + 'Name "{{name}}" exists at project level - existing subagent will take precedence': + 'Имя "{{name}}" существует на уровне проекта - существующий подагент будет иметь приоритет', + 'Description is over {{length}} characters': + 'Описание превышает {{length}} символов', + 'System prompt is over {{length}} characters': + 'Системный промпт превышает {{length}} символов', + // Агенты - Шаги мастера создания + 'Step {{n}}: Choose Location': 'Шаг {{n}}: Выберите расположение', + 'Step {{n}}: Choose Generation Method': 'Шаг {{n}}: Выберите метод генерации', + 'Generate with Qwen Code (Recommended)': + 'Сгенерировать с помощью Qwen Code (Рекомендуется)', + 'Manual Creation': 'Ручное создание', + 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)': + 'Опишите, что должен делать этот подагент и когда его следует использовать. (Будьте подробны для лучших результатов)', + 'e.g., Expert code reviewer that reviews code based on best practices...': + 'например, Экспертный ревьювер кода, проверяющий код на соответствие лучшим практикам...', + 'Generating subagent configuration...': 'Генерация конфигурации подагента...', + 'Failed to generate subagent: {{error}}': + 'Не удалось сгенерировать подагента: {{error}}', + 'Step {{n}}: Describe Your Subagent': 'Шаг {{n}}: Опишите подагента', + 'Step {{n}}: Enter Subagent Name': 'Шаг {{n}}: Введите имя подагента', + 'Step {{n}}: Enter System Prompt': 'Шаг {{n}}: Введите системный промпт', + 'Step {{n}}: Enter Description': 'Шаг {{n}}: Введите описание', + // Агенты - Выбор инструментов + 'Step {{n}}: Select Tools': 'Шаг {{n}}: Выберите инструменты', + 'All Tools (Default)': 'Все инструменты (по умолчанию)', + 'All Tools': 'Все инструменты', + 'Read-only Tools': 'Инструменты только для чтения', + 'Read & Edit Tools': 'Инструменты для чтения и редактирования', + 'Read & Edit & Execution Tools': + 'Инструменты для чтения, редактирования и выполнения', + 'All tools selected, including MCP tools': + 'Все инструменты выбраны, включая инструменты MCP', + 'Selected tools:': 'Выбранные инструменты:', + 'Read-only tools:': 'Инструменты только для чтения:', + 'Edit tools:': 'Инструменты редактирования:', + 'Execution tools:': 'Инструменты выполнения:', + 'Step {{n}}: Choose Background Color': 'Шаг {{n}}: Выберите цвет фона', + 'Step {{n}}: Confirm and Save': 'Шаг {{n}}: Подтвердите и сохраните', + // Агенты - Навигация и инструкции + 'Esc to cancel': 'Esc для отмены', + 'Press Enter to save, e to save and edit, Esc to go back': + 'Enter для сохранения, e для сохранения и редактирования, Esc для возврата', + 'Press Enter to continue, {{navigation}}Esc to {{action}}': + 'Enter для продолжения, {{navigation}}Esc для {{action}}', + cancel: 'отмены', + 'go back': 'возврата', + '↑↓ to navigate, ': '↑↓ для навигации, ', + 'Enter a clear, unique name for this subagent.': + 'Введите четкое, уникальное имя для этого подагента.', + 'e.g., Code Reviewer': 'например, Ревьювер кода', + 'Name cannot be empty.': 'Имя не может быть пустым.', + "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.": + 'Напишите системный промпт, определяющий поведение подагента. Будьте подробны для лучших результатов.', + 'e.g., You are an expert code reviewer...': + 'например, Вы экспертный ревьювер кода...', + 'System prompt cannot be empty.': 'Системный промпт не может быть пустым.', + 'Describe when and how this subagent should be used.': + 'Опишите, когда и как следует использовать этого подагента.', + 'e.g., Reviews code for best practices and potential bugs.': + 'например, Проверяет код на соответствие лучшим практикам и потенциальные ошибки.', + 'Description cannot be empty.': 'Описание не может быть пустым.', + 'Failed to launch editor: {{error}}': + 'Не удалось запустить редактор: {{error}}', + 'Failed to save and edit subagent: {{error}}': + 'Не удалось сохранить и отредактировать подагента: {{error}}', + + // ============================================================================ + // Команды - Общие (продолжение) + // ============================================================================ + 'View and edit Qwen Code settings': 'Просмотр и изменение настроек Qwen Code', + Settings: 'Настройки', + '(Use Enter to select{{tabText}})': '(Enter для выбора{{tabText}})', + ', Tab to change focus': ', Tab для смены фокуса', + 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.': + 'Для применения изменений необходимо перезапустить Qwen Code. Нажмите r для выхода и применения изменений.', + + // ============================================================================ + // Метки настроек + // ============================================================================ + 'Vim Mode': 'Режим Vim', + 'Disable Auto Update': 'Отключить автообновление', + 'Enable Prompt Completion': 'Включить автодополнение промптов', + 'Debug Keystroke Logging': 'Логирование нажатий клавиш для отладки', + Language: 'Язык', + 'Output Format': 'Формат вывода', + 'Hide Window Title': 'Скрыть заголовок окна', + 'Show Status in Title': 'Показывать статус в заголовке', + 'Hide Tips': 'Скрыть подсказки', + 'Hide Banner': 'Скрыть баннер', + 'Hide Context Summary': 'Скрыть сводку контекста', + 'Hide CWD': 'Скрыть текущую директорию', + 'Hide Sandbox Status': 'Скрыть статус песочницы', + 'Hide Model Info': 'Скрыть информацию о модели', + 'Hide Footer': 'Скрыть нижний колонтитул', + 'Show Memory Usage': 'Показывать использование памяти', + 'Show Line Numbers': 'Показывать номера строк', + 'Show Citations': 'Показывать цитаты', + 'Custom Witty Phrases': 'Пользовательские остроумные фразы', + 'Enable Welcome Back': 'Включить приветствие при возврате', + 'Disable Loading Phrases': 'Отключить фразы при загрузке', + 'Screen Reader Mode': 'Режим программы чтения с экрана', + 'IDE Mode': 'Режим IDE', + 'Max Session Turns': 'Макс. количество ходов сессии', + 'Skip Next Speaker Check': 'Пропустить проверку следующего говорящего', + 'Skip Loop Detection': 'Пропустить обнаружение циклов', + 'Skip Startup Context': 'Пропустить начальный контекст', + 'Enable OpenAI Logging': 'Включить логирование OpenAI', + 'OpenAI Logging Directory': 'Директория логов OpenAI', + Timeout: 'Таймаут', + 'Max Retries': 'Макс. количество попыток', + 'Disable Cache Control': 'Отключить управление кэшем', + 'Memory Discovery Max Dirs': 'Макс. директорий для поиска в памяти', + 'Load Memory From Include Directories': + 'Загружать память из включенных директорий', + 'Respect .gitignore': 'Учитывать .gitignore', + 'Respect .qwenignore': 'Учитывать .qwenignore', + 'Enable Recursive File Search': 'Включить рекурсивный поиск файлов', + 'Disable Fuzzy Search': 'Отключить нечеткий поиск', + 'Enable Interactive Shell': 'Включить интерактивный терминал', + 'Show Color': 'Показывать цвета', + 'Auto Accept': 'Автоподтверждение', + 'Use Ripgrep': 'Использовать Ripgrep', + 'Use Builtin Ripgrep': 'Использовать встроенный Ripgrep', + 'Enable Tool Output Truncation': 'Включить обрезку вывода инструментов', + 'Tool Output Truncation Threshold': 'Порог обрезки вывода инструментов', + 'Tool Output Truncation Lines': 'Лимит строк вывода инструментов', + 'Folder Trust': 'Доверие к папке', + 'Vision Model Preview': 'Визуальная модель (предпросмотр)', + // Варианты перечислений настроек + 'Auto (detect from system)': 'Авто (определить из системы)', + Text: 'Текст', + JSON: 'JSON', + Plan: 'План', + Default: 'По умолчанию', + 'Auto Edit': 'Авторедактирование', + YOLO: 'YOLO', + 'toggle vim mode on/off': 'Включение/выключение режима vim', + 'check session stats. Usage: /stats [model|tools]': + 'Просмотр статистики сессии. Использование: /stats [model|tools]', + 'Show model-specific usage statistics.': + 'Показать статистику использования модели.', + 'Show tool-specific usage statistics.': + 'Показать статистику использования инструментов.', + 'exit the cli': 'Выход из CLI', + 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers': + 'Показать настроенные MCP-серверы и инструменты, или авторизоваться на серверах с поддержкой OAuth', + 'Manage workspace directories': + 'Управление директориями рабочего пространства', + 'Add directories to the workspace. Use comma to separate multiple paths': + 'Добавить директории в рабочее пространство. Используйте запятую для разделения путей', + 'Show all directories in the workspace': + 'Показать все директории в рабочем пространстве', + 'set external editor preference': + 'Установка предпочитаемого внешнего редактора', + 'Manage extensions': 'Управление расширениями', + 'List active extensions': 'Показать активные расширения', + 'Update extensions. Usage: update |--all': + 'Обновить расширения. Использование: update |--all', + 'manage IDE integration': 'Управление интеграцией с IDE', + 'check status of IDE integration': 'Проверить статус интеграции с IDE', + 'install required IDE companion for {{ideName}}': + 'Установить необходимый компаньон IDE для {{ideName}}', + 'enable IDE integration': 'Включение интеграции с IDE', + 'disable IDE integration': 'Отключение интеграции с 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.': + 'Интеграция с IDE не поддерживается в вашем окружении. Для использования этой функции запустите Qwen Code в одной из поддерживаемых IDE: VS Code или форках VS Code.', + 'Set up GitHub Actions': 'Настройка GitHub Actions', + 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)': + 'Настройка привязки клавиш терминала для многострочного ввода (VS Code, Cursor, Windsurf, Trae)', + 'Please restart your terminal for the changes to take effect.': + 'Пожалуйста, перезапустите терминал для применения изменений.', + 'Failed to configure terminal: {{error}}': + 'Не удалось настроить терминал: {{error}}', + 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.': + 'Не удалось определить путь конфигурации {{terminalName}} в Windows: переменная окружения APPDATA не установлена.', + '{{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 существует, но не является корректным массивом JSON. Пожалуйста, исправьте файл вручную или удалите его для автоматической настройки.', + 'File: {{file}}': 'Файл: {{file}}', + 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.': + 'Не удалось разобрать {{terminalName}} keybindings.json. Файл содержит некорректный JSON. Пожалуйста, исправьте файл вручную или удалите его для автоматической настройки.', + 'Error: {{error}}': 'Ошибка: {{error}}', + 'Shift+Enter binding already exists': 'Привязка Shift+Enter уже существует', + 'Ctrl+Enter binding already exists': 'Привязка Ctrl+Enter уже существует', + 'Existing keybindings detected. Will not modify to avoid conflicts.': + 'Обнаружены существующие привязки клавиш. Не будут изменены во избежание конфликтов.', + 'Please check and modify manually if needed: {{file}}': + 'Пожалуйста, проверьте и измените вручную при необходимости: {{file}}', + 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.': + 'Добавлены привязки Shift+Enter и Ctrl+Enter для {{terminalName}}.', + 'Modified: {{file}}': 'Изменено: {{file}}', + '{{terminalName}} keybindings already configured.': + 'Привязки клавиш {{terminalName}} уже настроены.', + 'Failed to configure {{terminalName}}.': + 'Не удалось настроить {{terminalName}}.', + 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).': + 'Ваш терминал уже настроен для оптимальной работы с многострочным вводом (Shift+Enter и Ctrl+Enter).', + 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.': + 'Не удалось определить тип терминала. Поддерживаемые терминалы: VS Code, Cursor, Windsurf и Trae.', + 'Terminal "{{terminal}}" is not supported yet.': + 'Терминал "{{terminal}}" еще не поддерживается.', + + // ============================================================================ + // Команды - Язык + // ============================================================================ + 'Invalid language. Available: en-US, zh-CN': + 'Неверный язык. Доступны: en-US, zh-CN, ru-RU', + 'Language subcommands do not accept additional arguments.': + 'Подкоманды языка не принимают дополнительных аргументов.', + 'Current UI language: {{lang}}': 'Текущий язык интерфейса: {{lang}}', + 'Current LLM output language: {{lang}}': 'Текущий язык вывода LLM: {{lang}}', + 'LLM output language not set': 'Язык вывода LLM не установлен', + 'Set UI language': 'Установка языка интерфейса', + 'Set LLM output language': 'Установка языка вывода LLM', + 'Usage: /language ui [zh-CN|en-US]': + 'Использование: /language ui [zh-CN|en-US|ru-RU]', + 'Usage: /language output ': 'Использование: /language output ', + 'Example: /language output 中文': 'Пример: /language output 中文', + 'Example: /language output English': 'Пример: /language output English', + 'Example: /language output 日本語': 'Пример: /language output 日本語', + 'UI language changed to {{lang}}': 'Язык интерфейса изменен на {{lang}}', + 'LLM output language rule file generated at {{path}}': + 'Файл правил языка вывода LLM создан в {{path}}', + 'Please restart the application for the changes to take effect.': + 'Пожалуйста, перезапустите приложение для применения изменений.', + 'Failed to generate LLM output language rule file: {{error}}': + 'Не удалось создать файл правил языка вывода LLM: {{error}}', + 'Invalid command. Available subcommands:': + 'Неверная команда. Доступные подкоманды:', + 'Available subcommands:': 'Доступные подкоманды:', + 'To request additional UI language packs, please open an issue on GitHub.': + 'Для запроса дополнительных языковых пакетов интерфейса, пожалуйста, создайте обращение на GitHub.', + 'Available options:': 'Доступные варианты:', + ' - zh-CN: Simplified Chinese': ' - zh-CN: Упрощенный китайский', + ' - en-US: English': ' - en-US: Английский', + ' - ru-RU: Russian': ' - ru-RU: Русский', + 'Set UI language to Simplified Chinese (zh-CN)': + 'Установить язык интерфейса на упрощенный китайский (zh-CN)', + 'Set UI language to English (en-US)': + 'Установить язык интерфейса на английский (en-US)', + + // ============================================================================ + // Команды - Режим подтверждения + // ============================================================================ + 'Approval Mode': 'Режим подтверждения', + 'Current approval mode: {{mode}}': 'Текущий режим подтверждения: {{mode}}', + 'Available approval modes:': 'Доступные режимы подтверждения:', + 'Approval mode changed to: {{mode}}': + 'Режим подтверждения изменен на: {{mode}}', + 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})': + 'Режим подтверждения изменен на: {{mode}} (сохранено в настройках {{scope}}{{location}})', + 'Usage: /approval-mode [--session|--user|--project]': + 'Использование: /approval-mode [--session|--user|--project]', + 'Scope subcommands do not accept additional arguments.': + 'Подкоманды области не принимают дополнительных аргументов.', + 'Plan mode - Analyze only, do not modify files or execute commands': + 'Режим планирования - только анализ, без изменения файлов или выполнения команд', + 'Default mode - Require approval for file edits or shell commands': + 'Режим по умолчанию - требуется подтверждение для редактирования файлов или команд терминала', + 'Auto-edit mode - Automatically approve file edits': + 'Режим авторедактирования - автоматическое подтверждение изменений файлов', + 'YOLO mode - Automatically approve all tools': + 'Режим YOLO - автоматическое подтверждение всех инструментов', + '{{mode}} mode': 'Режим {{mode}}', + 'Settings service is not available; unable to persist the approval mode.': + 'Служба настроек недоступна; невозможно сохранить режим подтверждения.', + 'Failed to save approval mode: {{error}}': + 'Не удалось сохранить режим подтверждения: {{error}}', + 'Failed to change approval mode: {{error}}': + 'Не удалось изменить режим подтверждения: {{error}}', + 'Apply to current session only (temporary)': + 'Применить только к текущей сессии (временно)', + 'Persist for this project/workspace': + 'Сохранить для этого проекта/рабочего пространства', + 'Persist for this user on this machine': + 'Сохранить для этого пользователя на этой машине', + 'Analyze only, do not modify files or execute commands': + 'Только анализ, без изменения файлов или выполнения команд', + 'Require approval for file edits or shell commands': + 'Требуется подтверждение для редактирования файлов или команд терминала', + 'Automatically approve file edits': + 'Автоматически подтверждать изменения файлов', + 'Automatically approve all tools': + 'Автоматически подтверждать все инструменты', + 'Workspace approval mode exists and takes priority. User-level change will have no effect.': + 'Режим подтверждения рабочего пространства существует и имеет приоритет. Изменение на уровне пользователя не будет иметь эффекта.', + '(Use Enter to select, Tab to change focus)': + '(Enter для выбора, Tab для смены фокуса)', + 'Apply To': 'Применить к', + 'User Settings': 'Настройки пользователя', + 'Workspace Settings': 'Настройки рабочего пространства', + + // ============================================================================ + // Команды - Память + // ============================================================================ + 'Commands for interacting with memory.': + 'Команды для взаимодействия с памятью', + 'Show the current memory contents.': 'Показать текущее содержимое памяти.', + 'Show project-level memory contents.': 'Показать память уровня проекта.', + 'Show global memory contents.': 'Показать глобальную память.', + 'Add content to project-level memory.': + 'Добавить содержимое в память уровня проекта.', + 'Add content to global memory.': 'Добавить содержимое в глобальную память.', + 'Refresh the memory from the source.': 'Обновить память из источника.', + 'Usage: /memory add --project ': + 'Использование: /memory add --project <текст для запоминания>', + 'Usage: /memory add --global ': + 'Использование: /memory add --global <текст для запоминания>', + 'Attempting to save to project memory: "{{text}}"': + 'Попытка сохранить в память проекта: "{{text}}"', + 'Attempting to save to global memory: "{{text}}"': + 'Попытка сохранить в глобальную память: "{{text}}"', + 'Current memory content from {{count}} file(s):': + 'Текущее содержимое памяти из {{count}} файла(ов):', + 'Memory is currently empty.': 'Память в настоящее время пуста.', + 'Project memory file not found or is currently empty.': + 'Файл памяти проекта не найден или в настоящее время пуст.', + 'Global memory file not found or is currently empty.': + 'Файл глобальной памяти не найден или в настоящее время пуст.', + 'Global memory is currently empty.': + 'Глобальная память в настоящее время пуста.', + 'Global memory content:\n\n---\n{{content}}\n---': + 'Содержимое глобальной памяти:\n\n---\n{{content}}\n---', + 'Project memory content from {{path}}:\n\n---\n{{content}}\n---': + 'Содержимое памяти проекта из {{path}}:\n\n---\n{{content}}\n---', + 'Project memory is currently empty.': + 'Память проекта в настоящее время пуста.', + 'Refreshing memory from source files...': + 'Обновление памяти из исходных файлов...', + 'Add content to the memory. Use --global for global memory or --project for project memory.': + 'Добавить содержимое в память. Используйте --global для глобальной памяти или --project для памяти проекта.', + 'Usage: /memory add [--global|--project] ': + 'Использование: /memory add [--global|--project] <текст для запоминания>', + 'Attempting to save to memory {{scope}}: "{{fact}}"': + 'Попытка сохранить в память {{scope}}: "{{fact}}"', + + // ============================================================================ + // Команды - MCP + // ============================================================================ + 'Authenticate with an OAuth-enabled MCP server': + 'Авторизоваться на MCP-сервере с поддержкой OAuth', + 'List configured MCP servers and tools': + 'Просмотр настроенных MCP-серверов и инструментов', + 'Restarts MCP servers.': 'Перезапустить MCP-серверы.', + 'Config not loaded.': 'Конфигурация не загружена.', + 'Could not retrieve tool registry.': + 'Не удалось получить реестр инструментов.', + 'No MCP servers configured with OAuth authentication.': + 'Нет MCP-серверов, настроенных с авторизацией OAuth.', + 'MCP servers with OAuth authentication:': 'MCP-серверы с авторизацией OAuth:', + 'Use /mcp auth to authenticate.': + 'Используйте /mcp auth <имя-сервера> для авторизации.', + "MCP server '{{name}}' not found.": "MCP-сервер '{{name}}' не найден.", + "Successfully authenticated and refreshed tools for '{{name}}'.": + "Успешно авторизовано и обновлены инструменты для '{{name}}'.", + "Failed to authenticate with MCP server '{{name}}': {{error}}": + "Не удалось авторизоваться на MCP-сервере '{{name}}': {{error}}", + "Re-discovering tools from '{{name}}'...": + "Повторное обнаружение инструментов от '{{name}}'...", + + // ============================================================================ + // Команды - Чат + // ============================================================================ + 'Manage conversation history.': 'Управление историей диалогов.', + 'List saved conversation checkpoints': + 'Показать сохраненные точки восстановления диалога', + 'No saved conversation checkpoints found.': + 'Не найдено сохраненных точек восстановления диалога.', + 'List of saved conversations:': 'Список сохраненных диалогов:', + 'Note: Newest last, oldest first': + 'Примечание: новые последними, старые первыми', + 'Save the current conversation as a checkpoint. Usage: /chat save ': + 'Сохранить текущий диалог как точку восстановления. Использование: /chat save <тег>', + 'Missing tag. Usage: /chat save ': + 'Отсутствует тег. Использование: /chat save <тег>', + 'Delete a conversation checkpoint. Usage: /chat delete ': + 'Удалить точку восстановления диалога. Использование: /chat delete <тег>', + 'Missing tag. Usage: /chat delete ': + 'Отсутствует тег. Использование: /chat delete <тег>', + "Conversation checkpoint '{{tag}}' has been deleted.": + "Точка восстановления диалога '{{tag}}' удалена.", + "Error: No checkpoint found with tag '{{tag}}'.": + "Ошибка: точка восстановления с тегом '{{tag}}' не найдена.", + 'Resume a conversation from a checkpoint. Usage: /chat resume ': + 'Возобновить диалог из точки восстановления. Использование: /chat resume <тег>', + 'Missing tag. Usage: /chat resume ': + 'Отсутствует тег. Использование: /chat resume <тег>', + 'No saved checkpoint found with tag: {{tag}}.': + 'Не найдена сохраненная точка восстановления с тегом: {{tag}}.', + 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?': + 'Точка восстановления с тегом {{tag}} уже существует. Перезаписать?', + 'No chat client available to save conversation.': + 'Нет доступного клиента чата для сохранения диалога.', + 'Conversation checkpoint saved with tag: {{tag}}.': + 'Точка восстановления диалога сохранена с тегом: {{tag}}.', + 'No conversation found to save.': 'Нет диалога для сохранения.', + 'No chat client available to share conversation.': + 'Нет доступного клиента чата для экспорта диалога.', + 'Invalid file format. Only .md and .json are supported.': + 'Неверный формат файла. Поддерживаются только .md и .json.', + 'Error sharing conversation: {{error}}': + 'Ошибка при экспорте диалога: {{error}}', + 'Conversation shared to {{filePath}}': 'Диалог экспортирован в {{filePath}}', + 'No conversation found to share.': 'Нет диалога для экспорта.', + 'Share the current conversation to a markdown or json file. Usage: /chat share <путь-к-файлу>': + 'Экспортировать текущий диалог в markdown или json файл. Использование: /chat share <путь-к-файлу>', + + // ============================================================================ + // Команды - Резюме + // ============================================================================ + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md': + 'Сгенерировать сводку проекта и сохранить её в .qwen/PROJECT_SUMMARY.md', + 'No chat client available to generate summary.': + 'Нет доступного чат-клиента для генерации сводки.', + 'Already generating summary, wait for previous request to complete': + 'Генерация сводки уже выполняется, дождитесь завершения предыдущего запроса', + 'No conversation found to summarize.': + 'Не найдено диалогов для создания сводки.', + 'Failed to generate project context summary: {{error}}': + 'Не удалось сгенерировать сводку контекста проекта: {{error}}', + + // ============================================================================ + // Команды - Модель + // ============================================================================ + 'Switch the model for this session': 'Переключение модели для этой сессии', + 'Content generator configuration not available.': + 'Конфигурация генератора содержимого недоступна.', + 'Authentication type not available.': 'Тип авторизации недоступен.', + 'No models available for the current authentication type ({{authType}}).': + 'Нет доступных моделей для текущего типа авторизации ({{authType}}).', + + // ============================================================================ + // Команды - Очистка + // ============================================================================ + 'Clearing terminal and resetting chat.': 'Очистка терминала и сброс чата.', + 'Clearing terminal.': 'Очистка терминала.', + + // ============================================================================ + // Команды - Сжатие + // ============================================================================ + 'Already compressing, wait for previous request to complete': + 'Уже выполняется сжатие, дождитесь завершения предыдущего запроса', + 'Failed to compress chat history.': 'Не удалось сжать историю чата.', + 'Failed to compress chat history: {{error}}': + 'Не удалось сжать историю чата: {{error}}', + 'Compressing chat history': 'Сжатие истории чата', + 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.': + 'История чата сжата с {{originalTokens}} до {{newTokens}} токенов.', + 'Compression was not beneficial for this history size.': + 'Сжатие не было полезным для этого размера истории.', + 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.': + 'Сжатие истории чата не уменьшило размер. Это может указывать на проблемы с промптом сжатия.', + 'Could not compress chat history due to a token counting error.': + 'Не удалось сжать историю чата из-за ошибки подсчета токенов.', + 'Chat history is already compressed.': 'История чата уже сжата.', + + // ============================================================================ + // Команды - Директория + // ============================================================================ + 'Configuration is not available.': 'Конфигурация недоступна.', + 'Please provide at least one path to add.': + 'Пожалуйста, укажите хотя бы один путь для добавления.', + 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.': + 'Команда /directory add не поддерживается в ограничительных профилях песочницы. Пожалуйста, используйте --include-directories при запуске сессии.', + "Error adding '{{path}}': {{error}}": + "Ошибка при добавлении '{{path}}': {{error}}", + 'Successfully added GEMINI.md files from the following directories if there are:\n- {{directories}}': + 'Успешно добавлены файлы GEMINI.md из следующих директорий (если они есть):\n- {{directories}}', + 'Error refreshing memory: {{error}}': + 'Ошибка при обновлении памяти: {{error}}', + 'Successfully added directories:\n- {{directories}}': + 'Успешно добавлены директории:\n- {{directories}}', + 'Current workspace directories:\n{{directories}}': + 'Текущие директории рабочего пространства:\n{{directories}}', + + // ============================================================================ + // Команды - Документация + // ============================================================================ + 'Please open the following URL in your browser to view the documentation:\n{{url}}': + 'Пожалуйста, откройте следующий URL в браузере для просмотра документации:\n{{url}}', + 'Opening documentation in your browser: {{url}}': + 'Открытие документации в браузере: {{url}}', + + // ============================================================================ + // Диалоги - Подтверждение инструментов + // ============================================================================ + 'Do you want to proceed?': 'Вы хотите продолжить?', + 'Yes, allow once': 'Да, разрешить один раз', + 'Allow always': 'Всегда разрешать', + No: 'Нет', + 'No (esc)': 'Нет (esc)', + 'Yes, allow always for this session': 'Да, всегда разрешать для этой сессии', + 'Modify in progress:': 'Идет изменение:', + 'Save and close external editor to continue': + 'Сохраните и закройте внешний редактор для продолжения', + 'Apply this change?': 'Применить это изменение?', + 'Yes, allow always': 'Да, всегда разрешать', + 'Modify with external editor': 'Изменить во внешнем редакторе', + 'No, suggest changes (esc)': 'Нет, предложить изменения (esc)', + "Allow execution of: '{{command}}'?": "Разрешить выполнение: '{{command}}'?", + 'Yes, allow always ...': 'Да, всегда разрешать ...', + 'Yes, and auto-accept edits': 'Да, и автоматически принимать правки', + 'Yes, and manually approve edits': 'Да, и вручную подтверждать правки', + 'No, keep planning (esc)': 'Нет, продолжить планирование (esc)', + 'URLs to fetch:': 'URL для загрузки:', + 'MCP Server: {{server}}': 'MCP-сервер: {{server}}', + 'Tool: {{tool}}': 'Инструмент: {{tool}}', + 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?': + 'Разрешить выполнение инструмента MCP "{{tool}}" с сервера "{{server}}"?', + 'Yes, always allow tool "{{tool}}" from server "{{server}}"': + 'Да, всегда разрешать инструмент "{{tool}}" с сервера "{{server}}"', + 'Yes, always allow all tools from server "{{server}}"': + 'Да, всегда разрешать все инструменты с сервера "{{server}}"', + + // ============================================================================ + // Диалоги - Подтверждение оболочки + // ============================================================================ + 'Shell Command Execution': 'Выполнение команды терминала', + 'A custom command wants to run the following shell commands:': + 'Пользовательская команда хочет выполнить следующие команды терминала:', + + // ============================================================================ + // Диалоги - Квота подписки Pro + // ============================================================================ + 'Pro quota limit reached for {{model}}.': + 'Исчерпана квота подписки Pro для {{model}}.', + 'Change auth (executes the /auth command)': + 'Изменить авторизацию (выполняет команду /auth)', + 'Continue with {{model}}': 'Продолжить с {{model}}', + + // ============================================================================ + // Диалоги - Приветствие при возвращении + // ============================================================================ + 'Current Plan:': 'Текущий план:', + 'Progress: {{done}}/{{total}} tasks completed': + 'Прогресс: {{done}}/{{total}} задач выполнено', + ', {{inProgress}} in progress': ', {{inProgress}} в процессе', + 'Pending Tasks:': 'Ожидающие задачи:', + 'What would you like to do?': 'Что вы хотите сделать?', + 'Choose how to proceed with your session:': + 'Выберите, как продолжить сессию:', + 'Start new chat session': 'Начать новую сессию чата', + 'Continue previous conversation': 'Продолжить предыдущий диалог', + '👋 Welcome back! (Last updated: {{timeAgo}})': + '👋 С возвращением! (Последнее обновление: {{timeAgo}})', + '🎯 Overall Goal:': '🎯 Общая цель:', + + // ============================================================================ + // Диалоги - Авторизация + // ============================================================================ + 'Get started': 'Начать', + 'How would you like to authenticate for this project?': + 'Как вы хотите авторизоваться для этого проекта?', + 'OpenAI API key is required to use OpenAI authentication.': + 'Для использования авторизации OpenAI требуется ключ API OpenAI.', + 'You must select an auth method to proceed. Press Ctrl+C again to exit.': + 'Вы должны выбрать метод авторизации для продолжения. Нажмите Ctrl+C снова для выхода.', + '(Use Enter to Set Auth)': '(Enter для установки авторизации)', + 'Terms of Services and Privacy Notice for Qwen Code': + 'Условия обслуживания и уведомление о конфиденциальности для Qwen Code', + 'Qwen OAuth': 'Qwen OAuth', + OpenAI: 'OpenAI', + 'Failed to login. Message: {{message}}': + 'Не удалось войти. Сообщение: {{message}}', + 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.': + 'Авторизация должна быть {{enforcedType}}, но вы сейчас используете {{currentType}}.', + 'Qwen OAuth authentication timed out. Please try again.': + 'Время ожидания авторизации Qwen OAuth истекло. Пожалуйста, попробуйте снова.', + 'Qwen OAuth authentication cancelled.': 'Авторизация Qwen OAuth отменена.', + 'Qwen OAuth Authentication': 'Авторизация Qwen OAuth', + 'Please visit this URL to authorize:': + 'Пожалуйста, посетите этот URL для авторизации:', + 'Or scan the QR code below:': 'Или отсканируйте QR-код ниже:', + 'Waiting for authorization': 'Ожидание авторизации', + 'Time remaining:': 'Осталось времени:', + '(Press ESC or CTRL+C to cancel)': '(Нажмите ESC или CTRL+C для отмены)', + 'Qwen OAuth Authentication Timeout': 'Таймаут авторизации Qwen OAuth', + 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.': + 'Токен OAuth истек (более {{seconds}} секунд). Пожалуйста, выберите метод авторизации снова.', + 'Press any key to return to authentication type selection.': + 'Нажмите любую клавишу для возврата к выбору типа авторизации.', + 'Waiting for Qwen OAuth authentication...': + 'Ожидание авторизации 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.': + 'Примечание: Ваш существующий ключ API в settings.json не будет удален при использовании Qwen OAuth. Вы можете переключиться обратно на авторизацию OpenAI позже при необходимости.', + 'Authentication timed out. Please try again.': + 'Время ожидания авторизации истекло. Пожалуйста, попробуйте снова.', + 'Waiting for auth... (Press ESC or CTRL+C to cancel)': + 'Ожидание авторизации... (Нажмите ESC или CTRL+C для отмены)', + 'Failed to authenticate. Message: {{message}}': + 'Не удалось авторизоваться. Сообщение: {{message}}', + 'Authenticated successfully with {{authType}} credentials.': + 'Успешно авторизовано с учетными данными {{authType}}.', + 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}': + 'Неверное значение QWEN_DEFAULT_AUTH_TYPE: "{{value}}". Допустимые значения: {{validValues}}', + 'OpenAI Configuration Required': 'Требуется конфигурация OpenAI', + 'Please enter your OpenAI configuration. You can get an API key from': + 'Пожалуйста, введите конфигурацию OpenAI. Вы можете получить ключ API на', + 'API Key:': 'Ключ API:', + 'Invalid credentials: {{errorMessage}}': + 'Неверные учетные данные: {{errorMessage}}', + 'Failed to validate credentials': 'Не удалось проверить учетные данные', + 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel': + 'Enter для продолжения, Tab/↑↓ для навигации, Esc для отмены', + + // ============================================================================ + // Диалоги - Модель + // ============================================================================ + 'Select Model': 'Выбрать модель', + '(Press Esc to close)': '(Нажмите Esc для закрытия)', + 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)': + 'Последняя модель Qwen Coder от Alibaba Cloud ModelStudio (версия: qwen3-coder-plus-2025-09-23)', + 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)': + 'Последняя модель Qwen Vision от Alibaba Cloud ModelStudio (версия: qwen3-vl-plus-2025-09-23)', + + // ============================================================================ + // Диалоги - Разрешения + // ============================================================================ + 'Manage folder trust settings': 'Управление настройками доверия к папкам', + + // ============================================================================ + // Строка состояния + // ============================================================================ + 'Using:': 'Используется:', + '{{count}} open file': '{{count}} открытый файл', + '{{count}} open files': '{{count}} открытых файла(ов)', + '(ctrl+g to view)': '(ctrl+g для просмотра)', + '{{count}} {{name}} file': '{{count}} файл {{name}}', + '{{count}} {{name}} files': '{{count}} файла(ов) {{name}}', + '{{count}} MCP server': '{{count}} MCP-сервер', + '{{count}} MCP servers': '{{count}} MCP-сервера(ов)', + '{{count}} Blocked': '{{count}} заблокирован(о)', + '(ctrl+t to view)': '(ctrl+t для просмотра)', + '(ctrl+t to toggle)': '(ctrl+t для переключения)', + 'Press Ctrl+C again to exit.': 'Нажмите Ctrl+C снова для выхода.', + 'Press Ctrl+D again to exit.': 'Нажмите Ctrl+D снова для выхода.', + 'Press Esc again to clear.': 'Нажмите Esc снова для очистки.', + + // ============================================================================ + // Статус MCP + // ============================================================================ + 'No MCP servers configured.': 'Не настроено MCP-серверов.', + 'Please view MCP documentation in your browser:': + 'Пожалуйста, просмотрите документацию MCP в браузере:', + 'or use the cli /docs command': 'или используйте команду cli /docs', + '⏳ MCP servers are starting up ({{count}} initializing)...': + '⏳ MCP-серверы запускаются ({{count}} инициализируется)...', + 'Note: First startup may take longer. Tool availability will update automatically.': + 'Примечание: Первый запуск может занять больше времени. Доступность инструментов обновится автоматически.', + 'Configured MCP servers:': 'Настроенные MCP-серверы:', + Ready: 'Готов', + 'Starting... (first startup may take longer)': + 'Запуск... (первый запуск может занять больше времени)', + Disconnected: 'Отключен', + '{{count}} tool': '{{count}} инструмент', + '{{count}} tools': '{{count}} инструмента(ов)', + '{{count}} prompt': '{{count}} промпт', + '{{count}} prompts': '{{count}} промпта(ов)', + '(from {{extensionName}})': '(от {{extensionName}})', + OAuth: 'OAuth', + 'OAuth expired': 'OAuth истек', + 'OAuth not authenticated': 'OAuth не авторизован', + 'tools and prompts will appear when ready': + 'инструменты и промпты появятся, когда будут готовы', + '{{count}} tools cached': '{{count}} инструмента(ов) в кэше', + 'Tools:': 'Инструменты:', + 'Parameters:': 'Параметры:', + 'Prompts:': 'Промпты:', + Blocked: 'Заблокировано', + '💡 Tips:': '💡 Подсказки:', + Use: 'Используйте', + 'to show server and tool descriptions': + 'для показа описаний сервера и инструментов', + 'to show tool parameter schemas': 'для показа схем параметров инструментов', + 'to hide descriptions': 'для скрытия описаний', + 'to authenticate with OAuth-enabled servers': + 'для авторизации на серверах с поддержкой OAuth', + Press: 'Нажмите', + 'to toggle tool descriptions on/off': + 'для переключения описаний инструментов', + "Starting OAuth authentication for MCP server '{{name}}'...": + "Начало авторизации OAuth для MCP-сервера '{{name}}'...", + 'Restarting MCP servers...': 'Перезапуск MCP-серверов...', + + // ============================================================================ + // Подсказки при запуске + // ============================================================================ + 'Tips for getting started:': 'Подсказки для начала работы:', + '1. Ask questions, edit files, or run commands.': + '1. Задавайте вопросы, редактируйте файлы или выполняйте команды.', + '2. Be specific for the best results.': + '2. Будьте конкретны для лучших результатов.', + 'files to customize your interactions with Qwen Code.': + 'файлы для настройки взаимодействия с Qwen Code.', + 'for more information.': 'для получения дополнительной информации.', + + // ============================================================================ + // Экран выхода / Статистика + // ============================================================================ + 'Agent powering down. Goodbye!': 'Агент завершает работу. До свидания!', + 'Interaction Summary': 'Сводка взаимодействия', + 'Session ID:': 'ID сессии:', + 'Tool Calls:': 'Вызовы инструментов:', + 'Success Rate:': 'Процент успеха:', + 'User Agreement:': 'Согласие пользователя:', + reviewed: 'проверено', + 'Code Changes:': 'Изменения кода:', + Performance: 'Производительность', + 'Wall Time:': 'Общее время:', + 'Agent Active:': 'Активность агента:', + 'API Time:': 'Время API:', + 'Tool Time:': 'Время инструментов:', + 'Session Stats': 'Статистика сессии', + 'Model Usage': 'Использование модели', + Reqs: 'Запросов', + 'Input Tokens': 'Входных токенов', + 'Output Tokens': 'Выходных токенов', + 'Savings Highlight:': 'Экономия:', + 'of input tokens were served from the cache, reducing costs.': + 'входных токенов обслужено из кэша, снижая затраты.', + 'Tip: For a full token breakdown, run `/stats model`.': + 'Подсказка: Для полной разбивки токенов выполните `/stats model`.', + 'Model Stats For Nerds': 'Статистика модели для гиков', + 'Tool Stats For Nerds': 'Статистика инструментов для гиков', + Metric: 'Метрика', + API: 'API', + Requests: 'Запросы', + Errors: 'Ошибки', + 'Avg Latency': 'Средняя задержка', + Tokens: 'Токены', + Total: 'Всего', + Prompt: 'Промпт', + Cached: 'Кэшировано', + Thoughts: 'Размышления', + Tool: 'Инструмент', + Output: 'Вывод', + 'No API calls have been made in this session.': + 'В этой сессии не было вызовов API.', + 'Tool Name': 'Имя инструмента', + Calls: 'Вызовы', + 'Success Rate': 'Процент успеха', + 'Avg Duration': 'Средняя длительность', + 'User Decision Summary': 'Сводка решений пользователя', + 'Total Reviewed Suggestions:': 'Всего проверено предложений:', + ' » Accepted:': ' » Принято:', + ' » Rejected:': ' » Отклонено:', + ' » Modified:': ' » Изменено:', + ' Overall Agreement Rate:': ' Общий процент согласия:', + 'No tool calls have been made in this session.': + 'В этой сессии не было вызовов инструментов.', + 'Session start time is unavailable, cannot calculate stats.': + 'Время начала сессии недоступно, невозможно рассчитать статистику.', + + // ============================================================================ + // Loading Phrases + // ============================================================================ + 'Waiting for user confirmation...': + 'Ожидание подтверждения от пользователя...', + '(esc to cancel, {{time}})': '(esc для отмены, {{time}})', + "I'm Feeling Lucky": 'Мне повезёт!', + 'Shipping awesomeness... ': 'Доставляем крутизну... ', + 'Painting the serifs back on...': 'Рисуем засечки на буквах...', + 'Navigating the slime mold...': 'Пробираемся через слизевиков..', + 'Consulting the digital spirits...': 'Советуемся с цифровыми духами...', + 'Reticulating splines...': 'Сглаживание сплайнов...', + 'Warming up the AI hamsters...': 'Разогреваем ИИ-хомячков...', + 'Asking the magic conch shell...': 'Спрашиваем волшебную ракушку...', + 'Generating witty retort...': 'Генерируем остроумный ответ...', + 'Polishing the algorithms...': 'Полируем алгоритмы...', + "Don't rush perfection (or my code)...": + 'Не торопите совершенство (или мой код)...', + 'Brewing fresh bytes...': 'Завариваем свежие байты...', + 'Counting electrons...': 'Пересчитываем электроны...', + 'Engaging cognitive processors...': 'Задействуем когнитивные процессоры...', + 'Checking for syntax errors in the universe...': + 'Ищем синтаксические ошибки во вселенной...', + 'One moment, optimizing humor...': 'Секундочку, оптимизируем юмор...', + 'Shuffling punchlines...': 'Перетасовываем панчлайны...', + 'Untangling neural nets...': 'Распутаваем нейросети...', + 'Compiling brilliance...': 'Компилируем гениальность...', + 'Loading wit.exe...': 'Загружаем yumor.exe...', + 'Summoning the cloud of wisdom...': 'Призываем облако мудрости...', + 'Preparing a witty response...': 'Готовим остроумный ответ...', + "Just a sec, I'm debugging reality...": 'Секунду, идёт отладка реальности...', + 'Confuzzling the options...': 'Запутываем варианты...', + 'Tuning the cosmic frequencies...': 'Настраиваем космические частоты...', + 'Crafting a response worthy of your patience...': + 'Создаем ответ, достойный вашего терпения...', + 'Compiling the 1s and 0s...': 'Компилируем единички и нолики...', + 'Resolving dependencies... and existential crises...': + 'Разрешаем зависимости... и экзистенциальные кризисы...', + 'Defragmenting memories... both RAM and personal...': + 'Дефрагментация памяти... и оперативной, и личной...', + 'Rebooting the humor module...': 'Перезагрузка модуля юмора...', + 'Caching the essentials (mostly cat memes)...': + 'Кэшируем самое важное (в основном мемы с котиками)...', + 'Optimizing for ludicrous speed': 'Оптимизация для безумной скорости', + "Swapping bits... don't tell the bytes...": + 'Меняем биты... только байтам не говорите...', + 'Garbage collecting... be right back...': 'Сборка мусора... скоро вернусь...', + 'Assembling the interwebs...': 'Сборка интернетов...', + 'Converting coffee into code...': 'Превращаем кофе в код...', + 'Updating the syntax for reality...': 'Обновляем синтаксис реальности...', + 'Rewiring the synapses...': 'Переподключаем синапсы...', + 'Looking for a misplaced semicolon...': 'Ищем лишнюю точку с запятой...', + "Greasin' the cogs of the machine...": 'Смазываем шестерёнки машины...', + 'Pre-heating the servers...': 'Разогреваем серверы...', + 'Calibrating the flux capacitor...': 'Калибруем потоковый накопитель...', + 'Engaging the improbability drive...': 'Включаем двигатель невероятности...', + 'Channeling the Force...': 'Направляем Силу...', + 'Aligning the stars for optimal response...': + 'Выравниваем звёзды для оптимального ответа...', + 'So say we all...': 'Так скажем мы все...', + 'Loading the next great idea...': 'Загрузка следующей великой идеи...', + "Just a moment, I'm in the zone...": 'Минутку, я в потоке...', + 'Preparing to dazzle you with brilliance...': + 'Готовлюсь ослепить вас гениальностью...', + "Just a tick, I'm polishing my wit...": 'Секунду, полирую остроумие...', + "Hold tight, I'm crafting a masterpiece...": 'Держитесь, создаю шедевр...', + "Just a jiffy, I'm debugging the universe...": + 'Мигом, отлаживаю вселенную...', + "Just a moment, I'm aligning the pixels...": 'Момент, выравниваю пиксели...', + "Just a sec, I'm optimizing the humor...": 'Секунду, оптимизирую юмор...', + "Just a moment, I'm tuning the algorithms...": + 'Момент, настраиваю алгоритмы...', + 'Warp speed engaged...': 'Варп-скорость включена...', + 'Mining for more Dilithium crystals...': 'Добываем кристаллы дилития...', + "Don't panic...": 'Без паники...', + 'Following the white rabbit...': 'Следуем за белым кроликом...', + 'The truth is in here... somewhere...': 'Истина где-то здесь... внутри...', + 'Blowing on the cartridge...': 'Продуваем картридж...', + 'Loading... Do a barrel roll!': 'Загрузка... Сделай бочку!', + 'Waiting for the respawn...': 'Ждем респауна...', + 'Finishing the Kessel Run in less than 12 parsecs...': + 'Делаем Дугу Кесселя менее чем за 12 парсеков...', + "The cake is not a lie, it's just still loading...": + 'Тортик — не ложь, он просто ещё грузится...', + 'Fiddling with the character creation screen...': + 'Возимся с экраном создания персонажа...', + "Just a moment, I'm finding the right meme...": + 'Минутку, ищу подходящий мем...', + "Pressing 'A' to continue...": "Нажимаем 'A' для продолжения...", + 'Herding digital cats...': 'Пасём цифровых котов...', + 'Polishing the pixels...': 'Полируем пиксели...', + 'Finding a suitable loading screen pun...': + 'Ищем подходящий каламбур для экрана загрузки...', + 'Distracting you with this witty phrase...': + 'Отвлекаем вас этой остроумной фразой...', + 'Almost there... probably...': 'Почти готово... вроде...', + 'Our hamsters are working as fast as they can...': + 'Наши хомячки работают изо всех сил...', + 'Giving Cloudy a pat on the head...': 'Гладим Облачко по голове...', + 'Petting the cat...': 'Гладим кота...', + 'Rickrolling my boss...': 'Рикроллим начальника...', + 'Never gonna give you up, never gonna let you down...': + 'Never gonna give you up, never gonna let you down...', + 'Slapping the bass...': 'Лабаем бас-гитару...', + 'Tasting the snozberries...': 'Пробуем снузберри на вкус...', + "I'm going the distance, I'm going for speed...": + 'Иду до конца, иду на скорость...', + 'Is this the real life? Is this just fantasy?...': + 'Is this the real life? Is this just fantasy?...', + "I've got a good feeling about this...": 'У меня хорошее предчувствие...', + 'Poking the bear...': 'Дразним медведя... (Не лезь...)', + 'Doing research on the latest memes...': 'Изучаем свежие мемы...', + 'Figuring out how to make this more witty...': + 'Думаем, как сделать это остроумнее...', + 'Hmmm... let me think...': 'Хмм... дайте подумать...', + 'What do you call a fish with no eyes? A fsh...': + 'Как называется бумеранг, который не возвращается? Палка...', + 'Why did the computer go to therapy? It had too many bytes...': + 'Почему компьютер простудился? Потому что оставил окна открытыми...', + "Why don't programmers like nature? It has too many bugs...": + 'Почему программисты не любят гулять на улице? Там среда не настроена...', + 'Why do programmers prefer dark mode? Because light attracts bugs...': + 'Почему программисты предпочитают тёмную тему? Потому что в темноте не видно багов...', + 'Why did the developer go broke? Because they used up all their cache...': + 'Почему разработчик разорился? Потому что потратил весь свой кэш...', + "What can you do with a broken pencil? Nothing, it's pointless...": + 'Что можно делать со сломанным карандашом? Ничего — он тупой...', + 'Applying percussive maintenance...': 'Провожу настройку методом тыка...', + 'Searching for the correct USB orientation...': + 'Ищем, какой стороной вставлять флешку...', + 'Ensuring the magic smoke stays inside the wires...': + 'Следим, чтобы волшебный дым не вышел из проводов...', + 'Rewriting in Rust for no particular reason...': + 'Переписываем всё на Rust без особой причины...', + 'Trying to exit Vim...': 'Пытаемся выйти из Vim...', + 'Spinning up the hamster wheel...': 'Раскручиваем колесо для хомяка...', + "That's not a bug, it's an undocumented feature...": 'Это не баг, а фича...', + 'Engage.': 'Поехали!', + "I'll be back... with an answer.": 'Я вернусь... с ответом.', + 'My other process is a TARDIS...': 'Мой другой процесс — это ТАРДИС...', + 'Communing with the machine spirit...': 'Общаемся с духом машины...', + 'Letting the thoughts marinate...': 'Даем мыслям замариноваться...', + 'Just remembered where I put my keys...': + 'Только что вспомнил, куда положил ключи...', + 'Pondering the orb...': 'Размышляю над сферой...', + "I've seen things you people wouldn't believe... like a user who reads loading messages.": + 'Я видел такое, во что вы, люди, просто не поверите... например, пользователя, читающего сообщения загрузки.', + 'Initiating thoughtful gaze...': 'Инициируем задумчивый взгляд...', + "What's a computer's favorite snack? Microchips.": + 'Что сервер заказывает в баре? Пинг-коладу.', + "Why do Java developers wear glasses? Because they don't C#.": + 'Почему Java-разработчики не убираются дома? Они ждут сборщик мусора...', + 'Charging the laser... pew pew!': 'Заряжаем лазер... пиу-пиу!', + 'Dividing by zero... just kidding!': 'Делим на ноль... шучу!', + 'Looking for an adult superviso... I mean, processing.': + 'Ищу взрослых для присмот... в смысле, обрабатываю.', + 'Making it go beep boop.': 'Делаем бип-буп.', + 'Buffering... because even AIs need a moment.': + 'Буферизация... даже ИИ нужно мгновение.', + 'Entangling quantum particles for a faster response...': + 'Запутываем квантовые частицы для быстрого ответа...', + 'Polishing the chrome... on the algorithms.': + 'Полируем хром... на алгоритмах.', + 'Are you not entertained? (Working on it!)': + 'Вы ещё не развлеклись?! Разве вы не за этим сюда пришли?!', + 'Summoning the code gremlins... to help, of course.': + 'Призываем гремлинов кода... для помощи, конечно же.', + 'Just waiting for the dial-up tone to finish...': + 'Ждем, пока закончится звук dial-up модема...', + 'Recalibrating the humor-o-meter.': 'Перекалибровка юморометра.', + 'My other loading screen is even funnier.': + 'Мой другой экран загрузки ещё смешнее.', + "Pretty sure there's a cat walking on the keyboard somewhere...": + 'Кажется, где-то по клавиатуре гуляет кот...', + 'Enhancing... Enhancing... Still loading.': + 'Улучшаем... Ещё улучшаем... Всё ещё грузится.', + "It's not a bug, it's a feature... of this loading screen.": + 'Это не баг, это фича... экрана загрузки.', + 'Have you tried turning it off and on again? (The loading screen, not me.)': + 'Пробовали выключить и включить снова? (Экран загрузки, не меня!)', + 'Constructing additional pylons...': 'Нужно построить больше пилонов...', +}; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index e82c586a..66be2343 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -821,6 +821,7 @@ export default { // Exit Screen / Stats // ============================================================================ 'Agent powering down. Goodbye!': 'Qwen Code 正在关闭,再见!', + 'To continue this session, run': '要继续此会话,请运行', 'Interaction Summary': '交互摘要', 'Session ID:': '会话 ID:', 'Tool Calls:': '工具调用:', diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts index 9d649b2f..7c8e6fc5 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.test.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts @@ -58,7 +58,6 @@ vi.mock('../ui/commands/authCommand.js', () => ({ authCommand: {} })); vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} })); vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} })); vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} })); -vi.mock('../ui/commands/corgiCommand.js', () => ({ corgiCommand: {} })); vi.mock('../ui/commands/docsCommand.js', () => ({ docsCommand: {} })); vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: {} })); vi.mock('../ui/commands/extensionsCommand.js', () => ({ diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 100fbef9..d3877a8a 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -15,7 +15,6 @@ import { bugCommand } from '../ui/commands/bugCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js'; import { copyCommand } from '../ui/commands/copyCommand.js'; -import { corgiCommand } from '../ui/commands/corgiCommand.js'; import { docsCommand } from '../ui/commands/docsCommand.js'; import { directoryCommand } from '../ui/commands/directoryCommand.js'; import { editorCommand } from '../ui/commands/editorCommand.js'; @@ -63,7 +62,6 @@ export class BuiltinCommandLoader implements ICommandLoader { clearCommand, compressCommand, copyCommand, - corgiCommand, docsCommand, directoryCommand, editorCommand, diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 59f26cf2..fd825b9d 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -56,10 +56,10 @@ export const createMockCommandContext = ( pendingItem: null, setPendingItem: vi.fn(), loadHistory: vi.fn(), - toggleCorgiMode: vi.fn(), toggleVimEnabled: vi.fn(), extensionsUpdateState: new Map(), setExtensionsUpdateState: vi.fn(), + reloadCommands: vi.fn(), // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any, session: { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index edda3d4d..ff16c53d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -136,7 +136,6 @@ export const AppContainer = (props: AppContainerProps) => { const { settings, config, initializationResult } = props; const historyManager = useHistory(); useMemoryMonitor(historyManager); - const [corgiMode, setCorgiMode] = useState(false); const [debugMessage, setDebugMessage] = useState(''); const [quittingMessages, setQuittingMessages] = useState< HistoryItem[] | null @@ -485,7 +484,6 @@ export const AppContainer = (props: AppContainerProps) => { }, 100); }, setDebugMessage, - toggleCorgiMode: () => setCorgiMode((prev) => !prev), dispatchExtensionStateUpdate, addConfirmUpdateExtensionRequest, openSubagentCreateDialog, @@ -498,7 +496,6 @@ export const AppContainer = (props: AppContainerProps) => { openSettingsDialog, openModelDialog, setDebugMessage, - setCorgiMode, dispatchExtensionStateUpdate, openPermissionsDialog, openApprovalModeDialog, @@ -945,6 +942,7 @@ export const AppContainer = (props: AppContainerProps) => { isFocused, streamingState, elapsedTime, + settings, }); // Dialog close functionality @@ -1218,7 +1216,6 @@ export const AppContainer = (props: AppContainerProps) => { qwenAuthState, editorError, isEditorDialogOpen, - corgiMode, debugMessage, quittingMessages, isSettingsDialogOpen, @@ -1309,7 +1306,6 @@ export const AppContainer = (props: AppContainerProps) => { qwenAuthState, editorError, isEditorDialogOpen, - corgiMode, debugMessage, quittingMessages, isSettingsDialogOpen, diff --git a/packages/cli/src/ui/commands/corgiCommand.test.ts b/packages/cli/src/ui/commands/corgiCommand.test.ts deleted file mode 100644 index 3c25e8cd..00000000 --- a/packages/cli/src/ui/commands/corgiCommand.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { corgiCommand } from './corgiCommand.js'; -import { type CommandContext } from './types.js'; -import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; - -describe('corgiCommand', () => { - let mockContext: CommandContext; - - beforeEach(() => { - mockContext = createMockCommandContext(); - vi.spyOn(mockContext.ui, 'toggleCorgiMode'); - }); - - it('should call the toggleCorgiMode function on the UI context', async () => { - if (!corgiCommand.action) { - throw new Error('The corgi command must have an action.'); - } - - await corgiCommand.action(mockContext, ''); - - expect(mockContext.ui.toggleCorgiMode).toHaveBeenCalledTimes(1); - }); - - it('should have the correct name and description', () => { - expect(corgiCommand.name).toBe('corgi'); - expect(corgiCommand.description).toBe('Toggles corgi mode.'); - }); -}); diff --git a/packages/cli/src/ui/commands/corgiCommand.ts b/packages/cli/src/ui/commands/corgiCommand.ts deleted file mode 100644 index 2da6ad3e..00000000 --- a/packages/cli/src/ui/commands/corgiCommand.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CommandKind, type SlashCommand } from './types.js'; - -export const corgiCommand: SlashCommand = { - name: 'corgi', - description: 'Toggles corgi mode.', - hidden: true, - kind: CommandKind.BUILT_IN, - action: (context, _args) => { - context.ui.toggleCorgiMode(); - }, -}; diff --git a/packages/cli/src/ui/commands/languageCommand.test.ts b/packages/cli/src/ui/commands/languageCommand.test.ts new file mode 100644 index 00000000..5a4f395b --- /dev/null +++ b/packages/cli/src/ui/commands/languageCommand.test.ts @@ -0,0 +1,600 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import * as fs from 'node:fs'; +import { type CommandContext, CommandKind } from './types.js'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; + +// Mock i18n module +vi.mock('../../i18n/index.js', () => ({ + setLanguageAsync: vi.fn().mockResolvedValue(undefined), + getCurrentLanguage: vi.fn().mockReturnValue('en'), + t: vi.fn((key: string) => key), +})); + +// Mock settings module to avoid Storage side effect +vi.mock('../../config/settings.js', () => ({ + SettingScope: { + User: 'user', + Workspace: 'workspace', + Default: 'default', + }, +})); + +// Mock fs module +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + mkdirSync: vi.fn(), + default: { + ...actual, + existsSync: vi.fn(), + readFileSync: vi.fn(), + writeFileSync: vi.fn(), + mkdirSync: vi.fn(), + }, + }; +}); + +// Mock Storage from core +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + Storage: { + getGlobalQwenDir: vi.fn().mockReturnValue('/mock/.qwen'), + getGlobalSettingsPath: vi + .fn() + .mockReturnValue('/mock/.qwen/settings.json'), + }, + }; +}); + +// Import modules after mocking +import * as i18n from '../../i18n/index.js'; +import { languageCommand } from './languageCommand.js'; + +describe('languageCommand', () => { + let mockContext: CommandContext; + + beforeEach(() => { + vi.clearAllMocks(); + mockContext = createMockCommandContext({ + services: { + config: { + getModel: vi.fn().mockReturnValue('test-model'), + }, + settings: { + merged: {}, + setValue: vi.fn(), + }, + }, + }); + + // Reset i18n mocks + vi.mocked(i18n.getCurrentLanguage).mockReturnValue('en'); + vi.mocked(i18n.t).mockImplementation((key: string) => key); + + // Reset fs mocks + vi.mocked(fs.existsSync).mockReturnValue(false); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('command metadata', () => { + it('should have the correct name', () => { + expect(languageCommand.name).toBe('language'); + }); + + it('should have a description', () => { + expect(languageCommand.description).toBeDefined(); + expect(typeof languageCommand.description).toBe('string'); + }); + + it('should be a built-in command', () => { + expect(languageCommand.kind).toBe(CommandKind.BUILT_IN); + }); + + it('should have subcommands', () => { + expect(languageCommand.subCommands).toBeDefined(); + expect(languageCommand.subCommands?.length).toBe(2); + }); + + it('should have ui and output subcommands', () => { + const subCommandNames = languageCommand.subCommands?.map((c) => c.name); + expect(subCommandNames).toContain('ui'); + expect(subCommandNames).toContain('output'); + }); + }); + + describe('main command action - no arguments', () => { + it('should show current language settings when no arguments provided', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Current UI language:'), + }); + }); + + it('should show available subcommands in help', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('/language ui'), + }); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('/language output'), + }); + }); + + it('should show LLM output language when set', async () => { + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.readFileSync).mockReturnValue( + '# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY', + ); + + // Make t() function handle interpolation for this test + vi.mocked(i18n.t).mockImplementation( + (key: string, params?: Record) => { + if (params && key.includes('{{lang}}')) { + return key.replace('{{lang}}', params['lang'] || ''); + } + return key; + }, + ); + + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Current UI language:'), + }); + // Verify it correctly parses "Chinese" from the template format + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Chinese'), + }); + }); + }); + + describe('main command action - config not available', () => { + it('should return error when config is null', async () => { + mockContext.services.config = null; + + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, ''); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: expect.stringContaining('Configuration not available'), + }); + }); + }); + + describe('/language ui subcommand', () => { + it('should show help when no language argument provided', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui'); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Usage: /language ui'), + }); + }); + + it('should set English with "en"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui en'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(mockContext.services.settings.setValue).toHaveBeenCalled(); + expect(mockContext.ui.reloadCommands).toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set English with "en-US"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui en-US'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set English with "english"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui english'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set Chinese with "zh"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui zh'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('zh'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set Chinese with "zh-CN"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui zh-CN'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('zh'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set Chinese with "chinese"', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui chinese'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('zh'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should return error for invalid language', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'ui invalid'); + + expect(i18n.setLanguageAsync).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: expect.stringContaining('Invalid language'), + }); + }); + + it('should persist setting to user scope', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + await languageCommand.action(mockContext, 'ui en'); + + expect(mockContext.services.settings.setValue).toHaveBeenCalledWith( + expect.anything(), // SettingScope.User + 'general.language', + 'en', + ); + }); + }); + + describe('/language output subcommand', () => { + it('should show help when no language argument provided', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'output'); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('Usage: /language output'), + }); + }); + + it('should create LLM output language rule file', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action( + mockContext, + 'output Chinese', + ); + + expect(fs.mkdirSync).toHaveBeenCalled(); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('output-language.md'), + expect.stringContaining('Chinese'), + 'utf-8', + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining( + 'LLM output language rule file generated', + ), + }); + }); + + it('should include restart notice in success message', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action( + mockContext, + 'output Japanese', + ); + + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('restart'), + }); + }); + + it('should handle file write errors gracefully', async () => { + vi.mocked(fs.writeFileSync).mockImplementation(() => { + throw new Error('Permission denied'); + }); + + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'output German'); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: expect.stringContaining('Failed to generate'), + }); + }); + }); + + describe('backward compatibility - direct language arguments', () => { + it('should set Chinese with direct "zh" argument', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'zh'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('zh'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should set English with direct "en" argument', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'en'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should return error for unknown direct argument', async () => { + if (!languageCommand.action) { + throw new Error('The language command must have an action.'); + } + + const result = await languageCommand.action(mockContext, 'unknown'); + + expect(i18n.setLanguageAsync).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: expect.stringContaining('Invalid command'), + }); + }); + }); + + describe('ui subcommand object', () => { + const uiSubcommand = languageCommand.subCommands?.find( + (c) => c.name === 'ui', + ); + + it('should have correct metadata', () => { + expect(uiSubcommand).toBeDefined(); + expect(uiSubcommand?.name).toBe('ui'); + expect(uiSubcommand?.kind).toBe(CommandKind.BUILT_IN); + }); + + it('should have nested language subcommands', () => { + const nestedNames = uiSubcommand?.subCommands?.map((c) => c.name); + expect(nestedNames).toContain('zh-CN'); + expect(nestedNames).toContain('en-US'); + }); + + it('should have action that sets language', async () => { + if (!uiSubcommand?.action) { + throw new Error('UI subcommand must have an action.'); + } + + const result = await uiSubcommand.action(mockContext, 'en'); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + }); + + describe('output subcommand object', () => { + const outputSubcommand = languageCommand.subCommands?.find( + (c) => c.name === 'output', + ); + + it('should have correct metadata', () => { + expect(outputSubcommand).toBeDefined(); + expect(outputSubcommand?.name).toBe('output'); + expect(outputSubcommand?.kind).toBe(CommandKind.BUILT_IN); + }); + + it('should have action that generates rule file', async () => { + if (!outputSubcommand?.action) { + throw new Error('Output subcommand must have an action.'); + } + + // Ensure mocks are properly set for this test + vi.mocked(fs.mkdirSync).mockImplementation(() => undefined); + vi.mocked(fs.writeFileSync).mockImplementation(() => undefined); + + const result = await outputSubcommand.action(mockContext, 'French'); + + expect(fs.writeFileSync).toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining( + 'LLM output language rule file generated', + ), + }); + }); + }); + + describe('nested ui language subcommands', () => { + const uiSubcommand = languageCommand.subCommands?.find( + (c) => c.name === 'ui', + ); + const zhCNSubcommand = uiSubcommand?.subCommands?.find( + (c) => c.name === 'zh-CN', + ); + const enUSSubcommand = uiSubcommand?.subCommands?.find( + (c) => c.name === 'en-US', + ); + + it('zh-CN should have aliases', () => { + expect(zhCNSubcommand?.altNames).toContain('zh'); + expect(zhCNSubcommand?.altNames).toContain('chinese'); + }); + + it('en-US should have aliases', () => { + expect(enUSSubcommand?.altNames).toContain('en'); + expect(enUSSubcommand?.altNames).toContain('english'); + }); + + it('zh-CN action should set Chinese', async () => { + if (!zhCNSubcommand?.action) { + throw new Error('zh-CN subcommand must have an action.'); + } + + const result = await zhCNSubcommand.action(mockContext, ''); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('zh'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('en-US action should set English', async () => { + if (!enUSSubcommand?.action) { + throw new Error('en-US subcommand must have an action.'); + } + + const result = await enUSSubcommand.action(mockContext, ''); + + expect(i18n.setLanguageAsync).toHaveBeenCalledWith('en'); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: expect.stringContaining('UI language changed'), + }); + }); + + it('should reject extra arguments', async () => { + if (!zhCNSubcommand?.action) { + throw new Error('zh-CN subcommand must have an action.'); + } + + const result = await zhCNSubcommand.action(mockContext, 'extra args'); + + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: expect.stringContaining('do not accept additional arguments'), + }); + }); + }); +}); diff --git a/packages/cli/src/ui/commands/languageCommand.ts b/packages/cli/src/ui/commands/languageCommand.ts index ba04920b..455465ab 100644 --- a/packages/cli/src/ui/commands/languageCommand.ts +++ b/packages/cli/src/ui/commands/languageCommand.ts @@ -81,8 +81,9 @@ function getCurrentLlmOutputLanguage(): string | null { if (fs.existsSync(filePath)) { try { const content = fs.readFileSync(filePath, 'utf-8'); - // Extract language name from the first line (e.g., "# Chinese Response Rules" -> "Chinese") - const match = content.match(/^#\s+(.+?)\s+Response Rules/i); + // Extract language name from the first line + // Template format: "# CRITICAL: Chinese Output Language Rule - HIGHEST PRIORITY" + const match = content.match(/^#.*?(\w+)\s+Output Language Rule/i); if (match) { return match[1]; } @@ -127,16 +128,17 @@ async function setUiLanguage( context.ui.reloadCommands(); // Map language codes to friendly display names - const langDisplayNames: Record = { + const langDisplayNames: Partial> = { zh: '中文(zh-CN)', en: 'English(en-US)', + ru: 'Русский (ru-RU)', }; return { type: 'message', messageType: 'info', content: t('UI language changed to {{lang}}', { - lang: langDisplayNames[lang], + lang: langDisplayNames[lang] || lang, }), }; } @@ -216,7 +218,7 @@ export const languageCommand: SlashCommand = { : t('LLM output language not set'), '', t('Available subcommands:'), - ` /language ui [zh-CN|en-US] - ${t('Set UI language')}`, + ` /language ui [zh-CN|en-US|ru-RU] - ${t('Set UI language')}`, ` /language output - ${t('Set LLM output language')}`, ].join('\n'); @@ -232,7 +234,7 @@ export const languageCommand: SlashCommand = { const subcommand = parts[0].toLowerCase(); if (subcommand === 'ui') { - // Handle /language ui [zh-CN|en-US] + // Handle /language ui [zh-CN|en-US|ru-RU] if (parts.length === 1) { // Show UI language subcommand help return { @@ -241,11 +243,12 @@ export const languageCommand: SlashCommand = { content: [ t('Set UI language'), '', - t('Usage: /language ui [zh-CN|en-US]'), + t('Usage: /language ui [zh-CN|en-US|ru-RU]'), '', t('Available options:'), t(' - zh-CN: Simplified Chinese'), t(' - en-US: English'), + t(' - ru-RU: Russian'), '', t( 'To request additional UI language packs, please open an issue on GitHub.', @@ -266,11 +269,18 @@ export const languageCommand: SlashCommand = { langArg === 'zh-cn' ) { targetLang = 'zh'; + } else if ( + langArg === 'ru' || + langArg === 'ru-RU' || + langArg === 'russian' || + langArg === 'русский' + ) { + targetLang = 'ru'; } else { return { type: 'message', messageType: 'error', - content: t('Invalid language. Available: en-US, zh-CN'), + content: t('Invalid language. Available: en-US, zh-CN, ru-RU'), }; } @@ -307,13 +317,20 @@ export const languageCommand: SlashCommand = { langArg === 'zh-cn' ) { targetLang = 'zh'; + } else if ( + langArg === 'ru' || + langArg === 'ru-RU' || + langArg === 'russian' || + langArg === 'русский' + ) { + targetLang = 'ru'; } else { return { type: 'message', messageType: 'error', content: [ t('Invalid command. Available subcommands:'), - ' - /language ui [zh-CN|en-US] - ' + t('Set UI language'), + ' - /language ui [zh-CN|en-US|ru-RU] - ' + t('Set UI language'), ' - /language output - ' + t('Set LLM output language'), ].join('\n'), }; @@ -423,6 +440,29 @@ export const languageCommand: SlashCommand = { return setUiLanguage(context, 'en'); }, }, + { + name: 'ru-RU', + altNames: ['ru', 'russian', 'русский'], + get description() { + return t('Set UI language to Russian (ru-RU)'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + if (args.trim().length > 0) { + return { + type: 'message', + messageType: 'error', + content: t( + 'Language subcommands do not accept additional arguments.', + ), + }; + } + return setUiLanguage(context, 'ru'); + }, + }, ], }, { diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts index bac02070..6f0faae3 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.test.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts @@ -84,7 +84,7 @@ describe('setupGithubCommand', async () => { const expectedSubstrings = [ `set -eEuo pipefail`, - `fakeOpenCommand "https://github.com/google-github-actions/run-gemini-cli`, + `fakeOpenCommand "https://github.com/QwenLM/qwen-code-action`, ]; for (const substring of expectedSubstrings) { @@ -112,7 +112,7 @@ describe('setupGithubCommand', async () => { if (gitignoreExists) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); - expect(gitignoreContent).toContain('.gemini/'); + expect(gitignoreContent).toContain('.qwen/'); expect(gitignoreContent).toContain('gha-creds-*.json'); } }); @@ -135,7 +135,7 @@ describe('updateGitignore', () => { const gitignorePath = path.join(scratchDir, '.gitignore'); const content = await fs.readFile(gitignorePath, 'utf8'); - expect(content).toBe('.gemini/\ngha-creds-*.json\n'); + expect(content).toBe('.qwen/\ngha-creds-*.json\n'); }); it('appends entries to existing .gitignore file', async () => { @@ -148,13 +148,13 @@ describe('updateGitignore', () => { const content = await fs.readFile(gitignorePath, 'utf8'); expect(content).toBe( - '# Existing content\nnode_modules/\n\n.gemini/\ngha-creds-*.json\n', + '# Existing content\nnode_modules/\n\n.qwen/\ngha-creds-*.json\n', ); }); it('does not add duplicate entries', async () => { const gitignorePath = path.join(scratchDir, '.gitignore'); - const existingContent = '.gemini/\nsome-other-file\ngha-creds-*.json\n'; + const existingContent = '.qwen/\nsome-other-file\ngha-creds-*.json\n'; await fs.writeFile(gitignorePath, existingContent); await updateGitignore(scratchDir); @@ -166,7 +166,7 @@ describe('updateGitignore', () => { it('adds only missing entries when some already exist', async () => { const gitignorePath = path.join(scratchDir, '.gitignore'); - const existingContent = '.gemini/\nsome-other-file\n'; + const existingContent = '.qwen/\nsome-other-file\n'; await fs.writeFile(gitignorePath, existingContent); await updateGitignore(scratchDir); @@ -174,17 +174,17 @@ describe('updateGitignore', () => { const content = await fs.readFile(gitignorePath, 'utf8'); // Should add only the missing gha-creds-*.json entry - expect(content).toBe('.gemini/\nsome-other-file\n\ngha-creds-*.json\n'); + expect(content).toBe('.qwen/\nsome-other-file\n\ngha-creds-*.json\n'); expect(content).toContain('gha-creds-*.json'); - // Should not duplicate .gemini/ entry - expect((content.match(/\.gemini\//g) || []).length).toBe(1); + // Should not duplicate .qwen/ entry + expect((content.match(/\.qwen\//g) || []).length).toBe(1); }); it('does not get confused by entries in comments or as substrings', async () => { const gitignorePath = path.join(scratchDir, '.gitignore'); const existingContent = [ - '# This is a comment mentioning .gemini/ folder', - 'my-app.gemini/config', + '# This is a comment mentioning .qwen/ folder', + 'my-app.qwen/config', '# Another comment with gha-creds-*.json pattern', 'some-other-gha-creds-file.json', '', @@ -196,7 +196,7 @@ describe('updateGitignore', () => { const content = await fs.readFile(gitignorePath, 'utf8'); // Should add both entries since they don't actually exist as gitignore rules - expect(content).toContain('.gemini/'); + expect(content).toContain('.qwen/'); expect(content).toContain('gha-creds-*.json'); // Verify the entries were added (not just mentioned in comments) @@ -204,9 +204,9 @@ describe('updateGitignore', () => { .split('\n') .map((line) => line.split('#')[0].trim()) .filter((line) => line); - expect(lines).toContain('.gemini/'); + expect(lines).toContain('.qwen/'); expect(lines).toContain('gha-creds-*.json'); - expect(lines).toContain('my-app.gemini/config'); + expect(lines).toContain('my-app.qwen/config'); expect(lines).toContain('some-other-gha-creds-file.json'); }); diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 378f1101..b12268ed 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -23,11 +23,11 @@ import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js'; import { t } from '../../i18n/index.js'; export const GITHUB_WORKFLOW_PATHS = [ - 'gemini-dispatch/gemini-dispatch.yml', - 'gemini-assistant/gemini-invoke.yml', - 'issue-triage/gemini-triage.yml', - 'issue-triage/gemini-scheduled-triage.yml', - 'pr-review/gemini-review.yml', + 'qwen-dispatch/qwen-dispatch.yml', + 'qwen-assistant/qwen-invoke.yml', + 'issue-triage/qwen-triage.yml', + 'issue-triage/qwen-scheduled-triage.yml', + 'pr-review/qwen-review.yml', ]; // Generate OS-specific commands to open the GitHub pages needed for setup. @@ -50,9 +50,9 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { return commands; } -// Add Gemini CLI specific entries to .gitignore file +// Add Qwen Code specific entries to .gitignore file export async function updateGitignore(gitRepoRoot: string): Promise { - const gitignoreEntries = ['.gemini/', 'gha-creds-*.json']; + const gitignoreEntries = ['.qwen/', 'gha-creds-*.json']; const gitignorePath = path.join(gitRepoRoot, '.gitignore'); try { @@ -121,7 +121,7 @@ export const setupGithubCommand: SlashCommand = { // Get the latest release tag from GitHub const proxy = context?.services?.config?.getProxy(); const releaseTag = await getLatestGitHubRelease(proxy); - const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`; + const readmeUrl = `https://github.com/QwenLM/qwen-code-action/blob/${releaseTag}/README.md#quick-start`; // Create the .github/workflows directory to download the files into const githubWorkflowsDir = path.join(gitRepoRoot, '.github', 'workflows'); @@ -143,7 +143,7 @@ export const setupGithubCommand: SlashCommand = { for (const workflow of GITHUB_WORKFLOW_PATHS) { downloads.push( (async () => { - const endpoint = `https://raw.githubusercontent.com/google-github-actions/run-gemini-cli/refs/tags/${releaseTag}/examples/workflows/${workflow}`; + const endpoint = `https://raw.githubusercontent.com/QwenLM/qwen-code-action/refs/tags/${releaseTag}/examples/workflows/${workflow}`; const response = await fetch(endpoint, { method: 'GET', dispatcher: proxy ? new ProxyAgent(proxy) : undefined, @@ -204,8 +204,9 @@ export const setupGithubCommand: SlashCommand = { toolName: 'run_shell_command', toolArgs: { description: - 'Setting up GitHub Actions to triage issues and review PRs with Gemini.', + 'Setting up GitHub Actions to triage issues and review PRs with Qwen.', command, + is_background: false, }, }; }, diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index a2a352cb..f2ec2173 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -64,8 +64,6 @@ export interface CommandContext { * @param history The array of history items to load. */ loadHistory: UseHistoryManagerReturn['loadHistory']; - /** Toggles a special display mode. */ - toggleCorgiMode: () => void; toggleVimEnabled: () => Promise; setGeminiMdFileCount: (count: number) => void; reloadCommands: () => void; diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index 084cd746..d660d704 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -120,7 +120,6 @@ const createMockUIState = (overrides: Partial = {}): UIState => }, branchName: 'main', debugMessage: '', - corgiMode: false, errorCount: 0, nightly: false, isTrustedFolder: true, @@ -183,6 +182,7 @@ describe('Composer', () => { const { lastFrame } = renderComposer(uiState, settings); + // Smoke check that the Footer renders when enabled. expect(lastFrame()).toContain('Footer'); }); @@ -200,7 +200,6 @@ describe('Composer', () => { it('passes correct props to Footer including vim mode when enabled', async () => { const uiState = createMockUIState({ branchName: 'feature-branch', - corgiMode: true, errorCount: 2, sessionStats: { sessionId: 'test-session', diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 776817a6..71f278df 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -33,7 +33,6 @@ export const Footer: React.FC = () => { debugMode, branchName, debugMessage, - corgiMode, errorCount, showErrorDetails, promptTokenCount, @@ -45,7 +44,6 @@ export const Footer: React.FC = () => { debugMode: config.getDebugMode(), branchName: uiState.branchName, debugMessage: uiState.debugMessage, - corgiMode: uiState.corgiMode, errorCount: uiState.errorCount, showErrorDetails: uiState.showErrorDetails, promptTokenCount: uiState.sessionStats.lastPromptTokenCount, @@ -153,16 +151,6 @@ export const Footer: React.FC = () => { {showMemoryUsage && } - {corgiMode && ( - - | - - - - `) - - - )} {!showErrorDetails && errorCount > 0 && ( | diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index 7cca61ae..2c92af57 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -15,6 +15,7 @@ import type { } from '@qwen-code/qwen-code-core'; import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; import { renderWithProviders } from '../../test-utils/render.js'; +import { ConfigContext } from '../contexts/ConfigContext.js'; // Mock child components vi.mock('./messages/ToolGroupMessage.js', () => ({ @@ -22,7 +23,9 @@ vi.mock('./messages/ToolGroupMessage.js', () => ({ })); describe('', () => { - const mockConfig = {} as unknown as Config; + const mockConfig = { + getChatRecordingService: () => undefined, + } as unknown as Config; const baseItem = { id: 1, timestamp: 12345, @@ -133,9 +136,11 @@ describe('', () => { duration: '1s', }; const { lastFrame } = renderWithProviders( - - - , + + + + + , ); expect(lastFrame()).toContain('Agent powering down. Goodbye!'); }); diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index 766e851a..305b50b2 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest'; import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; import type { SessionMetrics } from '../contexts/SessionContext.js'; +import { ConfigContext } from '../contexts/ConfigContext.js'; vi.mock('../contexts/SessionContext.js', async (importOriginal) => { const actual = await importOriginal(); @@ -20,20 +21,36 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => { const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats); -const renderWithMockedStats = (metrics: SessionMetrics) => { +const renderWithMockedStats = ( + metrics: SessionMetrics, + sessionId: string = 'test-session-id-12345', + promptCount: number = 5, + chatRecordingEnabled: boolean = true, +) => { useSessionStatsMock.mockReturnValue({ stats: { + sessionId, sessionStartTime: new Date(), metrics, lastPromptTokenCount: 0, - promptCount: 5, + promptCount, }, - getPromptCount: () => 5, + getPromptCount: () => promptCount, startNewPrompt: vi.fn(), }); - return render(); + const mockConfig = { + getChatRecordingService: vi.fn(() => + chatRecordingEnabled ? ({} as never) : undefined, + ), + }; + + return render( + + + , + ); }; describe('', () => { @@ -70,6 +87,68 @@ describe('', () => { const output = lastFrame(); expect(output).toContain('Agent powering down. Goodbye!'); + expect(output).toContain('To continue this session, run'); + expect(output).toContain('qwen --resume test-session-id-12345'); expect(output).toMatchSnapshot(); }); + + it('does not show resume message when there are no messages', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, + }; + + // Pass promptCount = 0 to simulate no messages + const { lastFrame } = renderWithMockedStats( + metrics, + 'test-session-id-12345', + 0, + ); + const output = lastFrame(); + + expect(output).toContain('Agent powering down. Goodbye!'); + expect(output).not.toContain('To continue this session, run'); + expect(output).not.toContain('qwen --resume'); + }); + + it('does not show resume message when chat recording is disabled', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, + }; + + const { lastFrame } = renderWithMockedStats( + metrics, + 'test-session-id-12345', + 5, + false, + ); + const output = lastFrame(); + + expect(output).toContain('Agent powering down. Goodbye!'); + expect(output).not.toContain('To continue this session, run'); + expect(output).not.toContain('qwen --resume'); + }); }); diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx index c8d79e0e..b43f18bc 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx @@ -5,7 +5,11 @@ */ import type React from 'react'; +import { Box, Text } from 'ink'; import { StatsDisplay } from './StatsDisplay.js'; +import { useSessionStats } from '../contexts/SessionContext.js'; +import { useConfig } from '../contexts/ConfigContext.js'; +import { theme } from '../semantic-colors.js'; import { t } from '../../i18n/index.js'; interface SessionSummaryDisplayProps { @@ -14,9 +18,31 @@ interface SessionSummaryDisplayProps { export const SessionSummaryDisplay: React.FC = ({ duration, -}) => ( - -); +}) => { + const config = useConfig(); + const { stats } = useSessionStats(); + + // Only show the resume message if there were messages in the session AND + // chat recording is enabled (otherwise there is nothing to resume). + const hasMessages = stats.promptCount > 0; + const canResume = !!config.getChatRecordingService(); + + return ( + <> + + {hasMessages && canResume && ( + + + {t('To continue this session, run')}{' '} + + qwen --resume {stats.sessionId} + + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index f96ec33c..9e4d294e 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1461,7 +1461,7 @@ describe('SettingsDialog', () => { context: { fileFiltering: { respectGitIgnore: false, - respectQwemIgnore: true, + respectQwenIgnore: true, enableRecursiveFileSearch: false, disableFuzzySearch: true, }, @@ -1535,7 +1535,7 @@ describe('SettingsDialog', () => { loadMemoryFromIncludeDirectories: false, fileFiltering: { respectGitIgnore: false, - respectQwemIgnore: false, + respectQwenIgnore: false, enableRecursiveFileSearch: false, disableFuzzySearch: false, }, diff --git a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap index 7c925f72..dfa39ba8 100644 --- a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap @@ -6,7 +6,7 @@ exports[` > renders the summary display with a title 1` │ Agent powering down. Goodbye! │ │ │ │ Interaction Summary │ -│ Session ID: │ +│ Session ID: test-session-id-12345 │ │ Tool Calls: 0 ( ✓ 0 x 0 ) │ │ Success Rate: 0.0% │ │ Code Changes: +42 -15 │ @@ -26,5 +26,7 @@ exports[` > renders the summary display with a title 1` │ │ │ » Tip: For a full token breakdown, run \`/stats model\`. │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + +To continue this session, run qwen --resume test-session-id-12345" `; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index 7c2c04f9..fbc2244b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -14,14 +14,14 @@ exports[`SettingsDialog > Snapshot Tests > should render default state correctly │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -48,14 +48,14 @@ exports[`SettingsDialog > Snapshot Tests > should render focused on scope select │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -82,14 +82,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with accessibility sett │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -116,14 +116,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with all boolean settin │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false* │ -│ │ │ ▼ │ │ │ │ │ @@ -150,14 +150,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -184,14 +184,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with different scope se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -218,14 +218,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with file filtering set │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -252,14 +252,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with mixed boolean and │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -286,14 +286,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with tools and security │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title false │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips false │ -│ │ │ ▼ │ │ │ │ │ @@ -320,14 +320,14 @@ exports[`SettingsDialog > Snapshot Tests > should render with various boolean se │ │ │ Language Auto (detect from system) │ │ │ +│ Terminal Bell true │ +│ │ │ Output Format Text │ │ │ │ Hide Window Title true* │ │ │ │ Show Status in Title false │ │ │ -│ Hide Tips true* │ -│ │ │ ▼ │ │ │ │ │ diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index ac2f5f10..62e54204 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -54,7 +54,6 @@ export interface UIState { qwenAuthState: QwenAuthState; editorError: string | null; isEditorDialogOpen: boolean; - corgiMode: boolean; debugMessage: string; quittingMessages: HistoryItem[] | null; isSettingsDialogOpen: boolean; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index 55fec0c3..42ce4099 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -153,7 +153,6 @@ describe('useSlashCommandProcessor', () => { openModelDialog: mockOpenModelDialog, quit: mockSetQuittingMessages, setDebugMessage: vi.fn(), - toggleCorgiMode: vi.fn(), }, ), ); @@ -909,7 +908,6 @@ describe('useSlashCommandProcessor', () => { vi.fn(), // openThemeDialog mockOpenAuthDialog, vi.fn(), // openEditorDialog - vi.fn(), // toggleCorgiMode mockSetQuittingMessages, vi.fn(), // openSettingsDialog vi.fn(), // openModelSelectionDialog diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 553accb7..6439c934 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -68,7 +68,6 @@ interface SlashCommandProcessorActions { openApprovalModeDialog: () => void; quit: (messages: HistoryItem[]) => void; setDebugMessage: (message: string) => void; - toggleCorgiMode: () => void; dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void; addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void; openSubagentCreateDialog: () => void; @@ -206,7 +205,6 @@ export const useSlashCommandProcessor = ( setDebugMessage: actions.setDebugMessage, pendingItem, setPendingItem, - toggleCorgiMode: actions.toggleCorgiMode, toggleVimEnabled, setGeminiMdFileCount, reloadCommands, diff --git a/packages/cli/src/ui/hooks/useAttentionNotifications.test.ts b/packages/cli/src/ui/hooks/useAttentionNotifications.test.ts index 1475aa52..e8beb86f 100644 --- a/packages/cli/src/ui/hooks/useAttentionNotifications.test.ts +++ b/packages/cli/src/ui/hooks/useAttentionNotifications.test.ts @@ -15,6 +15,23 @@ import { LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS, useAttentionNotifications, } from './useAttentionNotifications.js'; +import type { LoadedSettings } from '../../config/settings.js'; + +const mockSettings: LoadedSettings = { + merged: { + general: { + terminalBell: true, + }, + }, +} as LoadedSettings; + +const mockSettingsDisabled: LoadedSettings = { + merged: { + general: { + terminalBell: false, + }, + }, +} as LoadedSettings; vi.mock('../../utils/attentionNotification.js', () => ({ notifyTerminalAttention: vi.fn(), @@ -40,6 +57,7 @@ describe('useAttentionNotifications', () => { isFocused: true, streamingState: StreamingState.Idle, elapsedTime: 0, + settings: mockSettings, ...props, }, }, @@ -53,11 +71,13 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.WaitingForConfirmation, elapsedTime: 0, + settings: mockSettings, }, }); expect(mockedNotify).toHaveBeenCalledWith( AttentionNotificationReason.ToolApproval, + { enabled: true }, ); }); @@ -72,6 +92,7 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.WaitingForConfirmation, elapsedTime: 0, + settings: mockSettings, }, }); @@ -86,6 +107,7 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.Responding, elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 5, + settings: mockSettings, }, }); @@ -94,11 +116,13 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.Idle, elapsedTime: 0, + settings: mockSettings, }, }); expect(mockedNotify).toHaveBeenCalledWith( AttentionNotificationReason.LongTaskComplete, + { enabled: true }, ); }); @@ -110,6 +134,7 @@ describe('useAttentionNotifications', () => { isFocused: true, streamingState: StreamingState.Responding, elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 2, + settings: mockSettings, }, }); @@ -118,6 +143,7 @@ describe('useAttentionNotifications', () => { isFocused: true, streamingState: StreamingState.Idle, elapsedTime: 0, + settings: mockSettings, }, }); @@ -135,6 +161,7 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.Responding, elapsedTime: 5, + settings: mockSettings, }, }); @@ -143,9 +170,30 @@ describe('useAttentionNotifications', () => { isFocused: false, streamingState: StreamingState.Idle, elapsedTime: 0, + settings: mockSettings, }, }); expect(mockedNotify).not.toHaveBeenCalled(); }); + + it('does not notify when terminalBell setting is disabled', () => { + const { rerender } = render({ + settings: mockSettingsDisabled, + }); + + rerender({ + hookProps: { + isFocused: false, + streamingState: StreamingState.WaitingForConfirmation, + elapsedTime: 0, + settings: mockSettingsDisabled, + }, + }); + + expect(mockedNotify).toHaveBeenCalledWith( + AttentionNotificationReason.ToolApproval, + { enabled: false }, + ); + }); }); diff --git a/packages/cli/src/ui/hooks/useAttentionNotifications.ts b/packages/cli/src/ui/hooks/useAttentionNotifications.ts index e632c827..7c5cd043 100644 --- a/packages/cli/src/ui/hooks/useAttentionNotifications.ts +++ b/packages/cli/src/ui/hooks/useAttentionNotifications.ts @@ -10,6 +10,7 @@ import { notifyTerminalAttention, AttentionNotificationReason, } from '../../utils/attentionNotification.js'; +import type { LoadedSettings } from '../../config/settings.js'; export const LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS = 20; @@ -17,13 +18,16 @@ interface UseAttentionNotificationsOptions { isFocused: boolean; streamingState: StreamingState; elapsedTime: number; + settings: LoadedSettings; } export const useAttentionNotifications = ({ isFocused, streamingState, elapsedTime, + settings, }: UseAttentionNotificationsOptions) => { + const terminalBellEnabled = settings?.merged?.general?.terminalBell ?? true; const awaitingNotificationSentRef = useRef(false); const respondingElapsedRef = useRef(0); @@ -33,14 +37,16 @@ export const useAttentionNotifications = ({ !isFocused && !awaitingNotificationSentRef.current ) { - notifyTerminalAttention(AttentionNotificationReason.ToolApproval); + notifyTerminalAttention(AttentionNotificationReason.ToolApproval, { + enabled: terminalBellEnabled, + }); awaitingNotificationSentRef.current = true; } if (streamingState !== StreamingState.WaitingForConfirmation || isFocused) { awaitingNotificationSentRef.current = false; } - }, [isFocused, streamingState]); + }, [isFocused, streamingState, terminalBellEnabled]); useEffect(() => { if (streamingState === StreamingState.Responding) { @@ -53,11 +59,13 @@ export const useAttentionNotifications = ({ respondingElapsedRef.current >= LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS; if (wasLongTask && !isFocused) { - notifyTerminalAttention(AttentionNotificationReason.LongTaskComplete); + notifyTerminalAttention(AttentionNotificationReason.LongTaskComplete, { + enabled: terminalBellEnabled, + }); } // Reset tracking for next task respondingElapsedRef.current = 0; return; } - }, [streamingState, elapsedTime, isFocused]); + }, [streamingState, elapsedTime, isFocused, terminalBellEnabled]); }; diff --git a/packages/cli/src/ui/noninteractive/nonInteractiveUi.ts b/packages/cli/src/ui/noninteractive/nonInteractiveUi.ts index fc75924a..77929333 100644 --- a/packages/cli/src/ui/noninteractive/nonInteractiveUi.ts +++ b/packages/cli/src/ui/noninteractive/nonInteractiveUi.ts @@ -20,7 +20,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] { loadHistory: (_newHistory) => {}, pendingItem: null, setPendingItem: (_item) => {}, - toggleCorgiMode: () => {}, toggleVimEnabled: async () => false, setGeminiMdFileCount: (_count) => {}, reloadCommands: () => {}, diff --git a/packages/cli/src/utils/attentionNotification.ts b/packages/cli/src/utils/attentionNotification.ts index 26dc2a25..e166444f 100644 --- a/packages/cli/src/utils/attentionNotification.ts +++ b/packages/cli/src/utils/attentionNotification.ts @@ -13,6 +13,7 @@ export enum AttentionNotificationReason { export interface TerminalNotificationOptions { stream?: Pick; + enabled?: boolean; } const TERMINAL_BELL = '\u0007'; @@ -28,6 +29,11 @@ export function notifyTerminalAttention( _reason: AttentionNotificationReason, options: TerminalNotificationOptions = {}, ): boolean { + // Check if terminal bell is enabled (default true for backwards compatibility) + if (options.enabled === false) { + return false; + } + const stream = options.stream ?? process.stdout; if (!stream?.write || stream.isTTY === false) { return false; diff --git a/packages/cli/src/utils/gitUtils.ts b/packages/cli/src/utils/gitUtils.ts index 11cf729b..35f58210 100644 --- a/packages/cli/src/utils/gitUtils.ts +++ b/packages/cli/src/utils/gitUtils.ts @@ -58,7 +58,7 @@ export const getLatestGitHubRelease = async ( try { const controller = new AbortController(); - const endpoint = `https://api.github.com/repos/google-github-actions/run-gemini-cli/releases/latest`; + const endpoint = `https://api.github.com/repos/QwenLM/qwen-code-action/releases/latest`; const response = await fetch(endpoint, { method: 'GET', @@ -83,9 +83,12 @@ export const getLatestGitHubRelease = async ( } return releaseTag; } catch (_error) { - console.debug(`Failed to determine latest run-gemini-cli release:`, _error); + console.debug( + `Failed to determine latest qwen-code-action release:`, + _error, + ); throw new Error( - `Unable to determine the latest run-gemini-cli release on GitHub.`, + `Unable to determine the latest qwen-code-action release on GitHub.`, ); } }; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index b507c9c5..073f2aa1 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -38,7 +38,6 @@ "src/ui/commands/clearCommand.test.ts", "src/ui/commands/compressCommand.test.ts", "src/ui/commands/copyCommand.test.ts", - "src/ui/commands/corgiCommand.test.ts", "src/ui/commands/docsCommand.test.ts", "src/ui/commands/editorCommand.test.ts", "src/ui/commands/extensionsCommand.test.ts", diff --git a/packages/core/package.json b/packages/core/package.json index 42def511..77fdbb28 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-core", - "version": "0.4.1", + "version": "0.5.1", "description": "Qwen Code Core", "repository": { "type": "git", diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 6383cb17..d3c9b14a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -318,6 +318,7 @@ export interface ConfigParameters { generationConfig?: Partial; cliVersion?: string; loadMemoryFromIncludeDirectories?: boolean; + chatRecording?: boolean; // Web search providers webSearch?: { provider: Array<{ @@ -349,6 +350,7 @@ export interface ConfigParameters { skipStartupContext?: boolean; sdkMode?: boolean; sessionSubagents?: SubagentConfig[]; + channel?: string; } function normalizeConfigOutputFormat( @@ -456,6 +458,7 @@ export class Config { | undefined; private readonly cliVersion?: string; private readonly experimentalZedIntegration: boolean = false; + private readonly chatRecordingEnabled: boolean; private readonly loadMemoryFromIncludeDirectories: boolean = false; private readonly webSearch?: { provider: Array<{ @@ -485,6 +488,7 @@ export class Config { private readonly enableToolOutputTruncation: boolean; private readonly eventEmitter?: EventEmitter; private readonly useSmartEdit: boolean; + private readonly channel: string | undefined; constructor(params: ConfigParameters) { this.sessionId = params.sessionId ?? randomUUID(); @@ -570,6 +574,8 @@ export class Config { ._generationConfig as ContentGeneratorConfig; this.cliVersion = params.cliVersion; + this.chatRecordingEnabled = params.chatRecording ?? true; + this.loadMemoryFromIncludeDirectories = params.loadMemoryFromIncludeDirectories ?? false; this.chatCompression = params.chatCompression; @@ -598,6 +604,7 @@ export class Config { this.enableToolOutputTruncation = params.enableToolOutputTruncation ?? true; this.useSmartEdit = params.useSmartEdit ?? false; this.extensionManagement = params.extensionManagement ?? true; + this.channel = params.channel; this.storage = new Storage(this.targetDir); this.vlmSwitchMode = params.vlmSwitchMode; this.inputFormat = params.inputFormat ?? InputFormat.TEXT; @@ -615,7 +622,9 @@ export class Config { setGlobalDispatcher(new ProxyAgent(this.getProxy() as string)); } this.geminiClient = new GeminiClient(this); - this.chatRecordingService = new ChatRecordingService(this); + this.chatRecordingService = this.chatRecordingEnabled + ? new ChatRecordingService(this) + : undefined; } /** @@ -735,7 +744,9 @@ export class Config { startNewSession(sessionId?: string): string { this.sessionId = sessionId ?? randomUUID(); this.sessionData = undefined; - this.chatRecordingService = new ChatRecordingService(this); + this.chatRecordingService = this.chatRecordingEnabled + ? new ChatRecordingService(this) + : undefined; if (this.initialized) { logStartSession(this, new StartSessionEvent(this)); } @@ -1144,6 +1155,10 @@ export class Config { return this.cliVersion; } + getChannel(): string | undefined { + return this.channel; + } + /** * Get the current FileSystemService */ @@ -1260,7 +1275,10 @@ export class Config { /** * Returns the chat recording service. */ - getChatRecordingService(): ChatRecordingService { + getChatRecordingService(): ChatRecordingService | undefined { + if (!this.chatRecordingEnabled) { + return undefined; + } if (!this.chatRecordingService) { this.chatRecordingService = new ChatRecordingService(this); } diff --git a/packages/core/src/core/openaiContentGenerator/converter.test.ts b/packages/core/src/core/openaiContentGenerator/converter.test.ts index e29b4640..5cd6af92 100644 --- a/packages/core/src/core/openaiContentGenerator/converter.test.ts +++ b/packages/core/src/core/openaiContentGenerator/converter.test.ts @@ -7,7 +7,13 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { OpenAIContentConverter } from './converter.js'; import type { StreamingToolCallParser } from './streamingToolCallParser.js'; -import type { GenerateContentParameters, Content } from '@google/genai'; +import { + Type, + type GenerateContentParameters, + type Content, + type Tool, + type CallableTool, +} from '@google/genai'; import type OpenAI from 'openai'; describe('OpenAIContentConverter', () => { @@ -202,4 +208,338 @@ describe('OpenAIContentConverter', () => { ); }); }); + + describe('convertGeminiToolsToOpenAI', () => { + it('should convert Gemini tools with parameters field', async () => { + const geminiTools = [ + { + functionDeclarations: [ + { + name: 'get_weather', + description: 'Get weather for a location', + parameters: { + type: Type.OBJECT, + properties: { + location: { type: Type.STRING }, + }, + required: ['location'], + }, + }, + ], + }, + ] as Tool[]; + + const result = await converter.convertGeminiToolsToOpenAI(geminiTools); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + type: 'function', + function: { + name: 'get_weather', + description: 'Get weather for a location', + parameters: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + required: ['location'], + }, + }, + }); + }); + + it('should convert MCP tools with parametersJsonSchema field', async () => { + // MCP tools use parametersJsonSchema which contains plain JSON schema (not Gemini types) + const mcpTools = [ + { + functionDeclarations: [ + { + name: 'read_file', + description: 'Read a file from disk', + parametersJsonSchema: { + type: 'object', + properties: { + path: { type: 'string' }, + }, + required: ['path'], + }, + }, + ], + }, + ] as Tool[]; + + const result = await converter.convertGeminiToolsToOpenAI(mcpTools); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + type: 'function', + function: { + name: 'read_file', + description: 'Read a file from disk', + parameters: { + type: 'object', + properties: { + path: { type: 'string' }, + }, + required: ['path'], + }, + }, + }); + }); + + it('should handle CallableTool by resolving tool function', async () => { + const callableTools = [ + { + tool: async () => ({ + functionDeclarations: [ + { + name: 'dynamic_tool', + description: 'A dynamically resolved tool', + parameters: { + type: Type.OBJECT, + properties: {}, + }, + }, + ], + }), + }, + ] as CallableTool[]; + + const result = await converter.convertGeminiToolsToOpenAI(callableTools); + + expect(result).toHaveLength(1); + expect(result[0].function.name).toBe('dynamic_tool'); + }); + + it('should skip functions without name or description', async () => { + const geminiTools = [ + { + functionDeclarations: [ + { + name: 'valid_tool', + description: 'A valid tool', + }, + { + name: 'missing_description', + // no description + }, + { + // no name + description: 'Missing name', + }, + ], + }, + ] as Tool[]; + + const result = await converter.convertGeminiToolsToOpenAI(geminiTools); + + expect(result).toHaveLength(1); + expect(result[0].function.name).toBe('valid_tool'); + }); + + it('should handle tools without functionDeclarations', async () => { + const emptyTools: Tool[] = [{} as Tool, { functionDeclarations: [] }]; + + const result = await converter.convertGeminiToolsToOpenAI(emptyTools); + + expect(result).toHaveLength(0); + }); + + it('should handle functions without parameters', async () => { + const geminiTools: Tool[] = [ + { + functionDeclarations: [ + { + name: 'no_params_tool', + description: 'A tool without parameters', + }, + ], + }, + ]; + + const result = await converter.convertGeminiToolsToOpenAI(geminiTools); + + expect(result).toHaveLength(1); + expect(result[0].function.parameters).toBeUndefined(); + }); + + it('should not mutate original parametersJsonSchema', async () => { + const originalSchema = { + type: 'object', + properties: { foo: { type: 'string' } }, + }; + const mcpTools: Tool[] = [ + { + functionDeclarations: [ + { + name: 'test_tool', + description: 'Test tool', + parametersJsonSchema: originalSchema, + }, + ], + } as Tool, + ]; + + const result = await converter.convertGeminiToolsToOpenAI(mcpTools); + + // Verify the result is a copy, not the same reference + expect(result[0].function.parameters).not.toBe(originalSchema); + expect(result[0].function.parameters).toEqual(originalSchema); + }); + }); + + describe('convertGeminiToolParametersToOpenAI', () => { + it('should convert type names to lowercase', () => { + const params = { + type: 'OBJECT', + properties: { + count: { type: 'INTEGER' }, + amount: { type: 'NUMBER' }, + name: { type: 'STRING' }, + }, + }; + + const result = converter.convertGeminiToolParametersToOpenAI(params); + + expect(result).toEqual({ + type: 'object', + properties: { + count: { type: 'integer' }, + amount: { type: 'number' }, + name: { type: 'string' }, + }, + }); + }); + + it('should convert string numeric constraints to numbers', () => { + const params = { + type: 'object', + properties: { + value: { + type: 'number', + minimum: '0', + maximum: '100', + multipleOf: '0.5', + }, + }, + }; + + const result = converter.convertGeminiToolParametersToOpenAI(params); + const properties = result?.['properties'] as Record; + + expect(properties?.['value']).toEqual({ + type: 'number', + minimum: 0, + maximum: 100, + multipleOf: 0.5, + }); + }); + + it('should convert string length constraints to integers', () => { + const params = { + type: 'object', + properties: { + text: { + type: 'string', + minLength: '1', + maxLength: '100', + }, + items: { + type: 'array', + minItems: '0', + maxItems: '10', + }, + }, + }; + + const result = converter.convertGeminiToolParametersToOpenAI(params); + const properties = result?.['properties'] as Record; + + expect(properties?.['text']).toEqual({ + type: 'string', + minLength: 1, + maxLength: 100, + }); + expect(properties?.['items']).toEqual({ + type: 'array', + minItems: 0, + maxItems: 10, + }); + }); + + it('should handle nested objects', () => { + const params = { + type: 'object', + properties: { + nested: { + type: 'object', + properties: { + deep: { + type: 'INTEGER', + minimum: '0', + }, + }, + }, + }, + }; + + const result = converter.convertGeminiToolParametersToOpenAI(params); + const properties = result?.['properties'] as Record; + const nested = properties?.['nested'] as Record; + const nestedProperties = nested?.['properties'] as Record< + string, + unknown + >; + + expect(nestedProperties?.['deep']).toEqual({ + type: 'integer', + minimum: 0, + }); + }); + + it('should handle arrays', () => { + const params = { + type: 'array', + items: { + type: 'INTEGER', + }, + }; + + const result = converter.convertGeminiToolParametersToOpenAI(params); + + expect(result).toEqual({ + type: 'array', + items: { + type: 'integer', + }, + }); + }); + + it('should return undefined for null or non-object input', () => { + expect( + converter.convertGeminiToolParametersToOpenAI( + null as unknown as Record, + ), + ).toBeNull(); + expect( + converter.convertGeminiToolParametersToOpenAI( + undefined as unknown as Record, + ), + ).toBeUndefined(); + }); + + it('should not mutate the original parameters', () => { + const original = { + type: 'OBJECT', + properties: { + count: { type: 'INTEGER' }, + }, + }; + const originalCopy = JSON.parse(JSON.stringify(original)); + + converter.convertGeminiToolParametersToOpenAI(original); + + expect(original).toEqual(originalCopy); + }); + }); }); diff --git a/packages/core/src/core/openaiContentGenerator/converter.ts b/packages/core/src/core/openaiContentGenerator/converter.ts index c046ec33..8187746d 100644 --- a/packages/core/src/core/openaiContentGenerator/converter.ts +++ b/packages/core/src/core/openaiContentGenerator/converter.ts @@ -199,13 +199,11 @@ export class OpenAIContentConverter { // Handle both Gemini tools (parameters) and MCP tools (parametersJsonSchema) if (func.parametersJsonSchema) { // MCP tool format - use parametersJsonSchema directly - if (func.parametersJsonSchema) { - // Create a shallow copy to avoid mutating the original object - const paramsCopy = { - ...(func.parametersJsonSchema as Record), - }; - parameters = paramsCopy; - } + // Create a shallow copy to avoid mutating the original object + const paramsCopy = { + ...(func.parametersJsonSchema as Record), + }; + parameters = paramsCopy; } else if (func.parameters) { // Gemini tool format - convert parameters to OpenAI format parameters = this.convertGeminiToolParametersToOpenAI( diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts index 2df72221..4a5b7748 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts @@ -130,10 +130,13 @@ export class DashScopeOpenAICompatibleProvider } buildMetadata(userPromptId: string): DashScopeRequestMetadata { + const channel = this.cliConfig.getChannel?.(); + return { metadata: { sessionId: this.cliConfig.getSessionId?.(), promptId: userPromptId, + ...(channel ? { channel } : {}), }, }; } diff --git a/packages/core/src/core/openaiContentGenerator/provider/types.ts b/packages/core/src/core/openaiContentGenerator/provider/types.ts index ea7c434d..362ec69a 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/types.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/types.ts @@ -28,5 +28,6 @@ export type DashScopeRequestMetadata = { metadata: { sessionId?: string; promptId: string; + channel?: string; }; }; diff --git a/packages/core/src/qwen/qwenOAuth2.test.ts b/packages/core/src/qwen/qwenOAuth2.test.ts index 23c26296..0c401f90 100644 --- a/packages/core/src/qwen/qwenOAuth2.test.ts +++ b/packages/core/src/qwen/qwenOAuth2.test.ts @@ -761,7 +761,6 @@ describe('getQwenOAuthClient', () => { }); it('should load cached credentials if available', async () => { - const fs = await import('node:fs'); const mockCredentials = { access_token: 'cached-token', refresh_token: 'cached-refresh', @@ -769,10 +768,6 @@ describe('getQwenOAuthClient', () => { expiry_date: Date.now() + 3600000, }; - vi.mocked(fs.promises.readFile).mockResolvedValue( - JSON.stringify(mockCredentials), - ); - // Mock SharedTokenManager to use cached credentials const mockTokenManager = { getValidCredentials: vi.fn().mockResolvedValue(mockCredentials), @@ -792,18 +787,6 @@ describe('getQwenOAuthClient', () => { }); it('should handle cached credentials refresh failure', async () => { - const fs = await import('node:fs'); - const mockCredentials = { - access_token: 'cached-token', - refresh_token: 'expired-refresh', - token_type: 'Bearer', - expiry_date: Date.now() + 3600000, // Valid expiry time so loadCachedQwenCredentials returns true - }; - - vi.mocked(fs.promises.readFile).mockResolvedValue( - JSON.stringify(mockCredentials), - ); - // Mock SharedTokenManager to fail with a specific error const mockTokenManager = { getValidCredentials: vi @@ -833,6 +816,35 @@ describe('getQwenOAuthClient', () => { SharedTokenManager.getInstance = originalGetInstance; }); + + it('should not start device flow when requireCachedCredentials is true', async () => { + // Make SharedTokenManager fail so we hit the fallback path + const mockTokenManager = { + getValidCredentials: vi + .fn() + .mockRejectedValue(new Error('No credentials')), + }; + + const originalGetInstance = SharedTokenManager.getInstance; + SharedTokenManager.getInstance = vi.fn().mockReturnValue(mockTokenManager); + + // If requireCachedCredentials is honored, device-flow network requests should not start + vi.mocked(global.fetch).mockResolvedValue({ ok: true } as Response); + + await expect( + import('./qwenOAuth2.js').then((module) => + module.getQwenOAuthClient(mockConfig, { + requireCachedCredentials: true, + }), + ), + ).rejects.toThrow( + 'No cached Qwen-OAuth credentials found. Please re-authenticate.', + ); + + expect(global.fetch).not.toHaveBeenCalled(); + + SharedTokenManager.getInstance = originalGetInstance; + }); }); describe('CredentialsClearRequiredError', () => { @@ -1574,178 +1586,6 @@ describe('Credential Caching Functions', () => { expect(updatedCredentials.access_token).toBe('new-token'); }); }); - - describe('loadCachedQwenCredentials', () => { - it('should load and validate cached credentials successfully', async () => { - const { promises: fs } = await import('node:fs'); - const mockCredentials = { - access_token: 'cached-token', - refresh_token: 'cached-refresh', - token_type: 'Bearer', - expiry_date: Date.now() + 3600000, - }; - - vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockCredentials)); - - // Test through getQwenOAuthClient which calls loadCachedQwenCredentials - const mockConfig = { - isBrowserLaunchSuppressed: vi.fn().mockReturnValue(true), - } as unknown as Config; - - // Make SharedTokenManager fail to test the fallback - const mockTokenManager = { - getValidCredentials: vi - .fn() - .mockRejectedValue(new Error('No cached creds')), - }; - - const originalGetInstance = SharedTokenManager.getInstance; - SharedTokenManager.getInstance = vi - .fn() - .mockReturnValue(mockTokenManager); - - // Mock successful auth flow after cache load fails - const mockAuthResponse = { - ok: true, - json: async () => ({ - device_code: 'test-device-code', - user_code: 'TEST123', - verification_uri: 'https://chat.qwen.ai/device', - verification_uri_complete: 'https://chat.qwen.ai/device?code=TEST123', - expires_in: 1800, - }), - }; - - const mockTokenResponse = { - ok: true, - json: async () => ({ - access_token: 'new-access-token', - refresh_token: 'new-refresh-token', - token_type: 'Bearer', - expires_in: 3600, - scope: 'openid profile email model.completion', - }), - }; - - global.fetch = vi - .fn() - .mockResolvedValueOnce(mockAuthResponse as Response) - .mockResolvedValue(mockTokenResponse as Response); - - try { - await import('./qwenOAuth2.js').then((module) => - module.getQwenOAuthClient(mockConfig), - ); - } catch { - // Expected to fail in test environment - } - - expect(fs.readFile).toHaveBeenCalled(); - SharedTokenManager.getInstance = originalGetInstance; - }); - - it('should handle invalid cached credentials gracefully', async () => { - const { promises: fs } = await import('node:fs'); - - // Mock file read to return invalid JSON - vi.mocked(fs.readFile).mockResolvedValue('invalid-json'); - - const mockConfig = { - isBrowserLaunchSuppressed: vi.fn().mockReturnValue(true), - } as unknown as Config; - - const mockTokenManager = { - getValidCredentials: vi - .fn() - .mockRejectedValue(new Error('No cached creds')), - }; - - const originalGetInstance = SharedTokenManager.getInstance; - SharedTokenManager.getInstance = vi - .fn() - .mockReturnValue(mockTokenManager); - - // Mock auth flow - const mockAuthResponse = { - ok: true, - json: async () => ({ - device_code: 'test-device-code', - user_code: 'TEST123', - verification_uri: 'https://chat.qwen.ai/device', - verification_uri_complete: 'https://chat.qwen.ai/device?code=TEST123', - expires_in: 1800, - }), - }; - - const mockTokenResponse = { - ok: true, - json: async () => ({ - access_token: 'new-token', - refresh_token: 'new-refresh', - token_type: 'Bearer', - expires_in: 3600, - }), - }; - - global.fetch = vi - .fn() - .mockResolvedValueOnce(mockAuthResponse as Response) - .mockResolvedValue(mockTokenResponse as Response); - - try { - await import('./qwenOAuth2.js').then((module) => - module.getQwenOAuthClient(mockConfig), - ); - } catch { - // Expected to fail in test environment - } - - SharedTokenManager.getInstance = originalGetInstance; - }); - - it('should handle file access errors', async () => { - const { promises: fs } = await import('node:fs'); - - vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found')); - - const mockConfig = { - isBrowserLaunchSuppressed: vi.fn().mockReturnValue(true), - } as unknown as Config; - - const mockTokenManager = { - getValidCredentials: vi - .fn() - .mockRejectedValue(new Error('No cached creds')), - }; - - const originalGetInstance = SharedTokenManager.getInstance; - SharedTokenManager.getInstance = vi - .fn() - .mockReturnValue(mockTokenManager); - - // Mock device flow to fail quickly - const mockAuthResponse = { - ok: true, - json: async () => ({ - error: 'invalid_request', - error_description: 'Invalid request parameters', - }), - }; - - global.fetch = vi.fn().mockResolvedValue(mockAuthResponse as Response); - - // Should proceed to device flow when cache loading fails - try { - await import('./qwenOAuth2.js').then((module) => - module.getQwenOAuthClient(mockConfig), - ); - } catch { - // Expected to fail in test environment - } - - SharedTokenManager.getInstance = originalGetInstance; - }); - }); }); describe('Enhanced Error Handling and Edge Cases', () => { diff --git a/packages/core/src/qwen/qwenOAuth2.ts b/packages/core/src/qwen/qwenOAuth2.ts index c4cfa933..77c5345a 100644 --- a/packages/core/src/qwen/qwenOAuth2.ts +++ b/packages/core/src/qwen/qwenOAuth2.ts @@ -514,26 +514,14 @@ export async function getQwenOAuthClient( } } - // If shared manager fails, check if we have cached credentials for device flow - if (await loadCachedQwenCredentials(client)) { - // We have cached credentials but they might be expired - // Try device flow instead of forcing refresh - const result = await authWithQwenDeviceFlow(client, config); - if (!result.success) { - // Use detailed error message if available, otherwise use default - const errorMessage = - result.message || 'Qwen OAuth authentication failed'; - throw new Error(errorMessage); - } - return client; - } - if (options?.requireCachedCredentials) { throw new Error( 'No cached Qwen-OAuth credentials found. Please re-authenticate.', ); } + // If we couldn't obtain valid credentials via SharedTokenManager, fall back to + // interactive device authorization (unless explicitly forbidden above). const result = await authWithQwenDeviceFlow(client, config); if (!result.success) { // Only emit timeout event if the failure reason is actually timeout @@ -689,6 +677,19 @@ async function authWithQwenDeviceFlow( // Cache the new tokens await cacheQwenCredentials(credentials); + // IMPORTANT: + // SharedTokenManager maintains an in-memory cache and throttles file checks. + // If we only write the creds file here, a subsequent `getQwenOAuthClient()` + // call in the same process (within the throttle window) may not re-read the + // updated file and could incorrectly re-trigger device auth. + // Clearing the cache forces the next call to reload from disk. + try { + SharedTokenManager.getInstance().clearCache(); + } catch { + // In unit tests we sometimes mock SharedTokenManager.getInstance() with a + // minimal stub; cache invalidation is best-effort and should not break auth. + } + // Emit auth progress success event qwenOAuth2Events.emit( QwenOAuth2Event.AuthProgress, @@ -847,27 +848,6 @@ async function authWithQwenDeviceFlow( } } -async function loadCachedQwenCredentials( - client: QwenOAuth2Client, -): Promise { - try { - const keyFile = getQwenCachedCredentialPath(); - const creds = await fs.readFile(keyFile, 'utf-8'); - const credentials = JSON.parse(creds) as QwenCredentials; - client.setCredentials(credentials); - - // Verify that the credentials are still valid - const { token } = await client.getAccessToken(); - if (!token) { - return false; - } - - return true; - } catch (_) { - return false; - } -} - async function cacheQwenCredentials(credentials: QwenCredentials) { const filePath = getQwenCachedCredentialPath(); try { @@ -913,6 +893,14 @@ export async function clearQwenCredentials(): Promise { } // Log other errors but don't throw - clearing credentials should be non-critical console.warn('Warning: Failed to clear cached Qwen credentials:', error); + } finally { + // Also clear SharedTokenManager in-memory cache to prevent stale credentials + // from being reused within the same process after the file is removed. + try { + SharedTokenManager.getInstance().clearCache(); + } catch { + // Best-effort; don't fail credential clearing if SharedTokenManager is mocked. + } } } diff --git a/packages/core/src/subagents/index.ts b/packages/core/src/subagents/index.ts index 5560b4fd..17c62a20 100644 --- a/packages/core/src/subagents/index.ts +++ b/packages/core/src/subagents/index.ts @@ -58,6 +58,7 @@ export type { SubAgentStartEvent, SubAgentRoundEvent, SubAgentStreamTextEvent, + SubAgentUsageEvent, SubAgentToolCallEvent, SubAgentToolResultEvent, SubAgentFinishEvent, diff --git a/packages/core/src/subagents/subagent-events.ts b/packages/core/src/subagents/subagent-events.ts index 3c93112d..1f793308 100644 --- a/packages/core/src/subagents/subagent-events.ts +++ b/packages/core/src/subagents/subagent-events.ts @@ -10,7 +10,7 @@ import type { ToolConfirmationOutcome, ToolResultDisplay, } from '../tools/tools.js'; -import type { Part } from '@google/genai'; +import type { Part, GenerateContentResponseUsageMetadata } from '@google/genai'; export type SubAgentEvent = | 'start' @@ -20,6 +20,7 @@ export type SubAgentEvent = | 'tool_call' | 'tool_result' | 'tool_waiting_approval' + | 'usage_metadata' | 'finish' | 'error'; @@ -31,6 +32,7 @@ export enum SubAgentEventType { TOOL_CALL = 'tool_call', TOOL_RESULT = 'tool_result', TOOL_WAITING_APPROVAL = 'tool_waiting_approval', + USAGE_METADATA = 'usage_metadata', FINISH = 'finish', ERROR = 'error', } @@ -57,6 +59,14 @@ export interface SubAgentStreamTextEvent { timestamp: number; } +export interface SubAgentUsageEvent { + subagentId: string; + round: number; + usage: GenerateContentResponseUsageMetadata; + durationMs?: number; + timestamp: number; +} + export interface SubAgentToolCallEvent { subagentId: string; round: number; diff --git a/packages/core/src/subagents/subagent-statistics.test.ts b/packages/core/src/subagents/subagent-statistics.test.ts index 5b4ae3c6..39ba70aa 100644 --- a/packages/core/src/subagents/subagent-statistics.test.ts +++ b/packages/core/src/subagents/subagent-statistics.test.ts @@ -50,6 +50,15 @@ describe('SubagentStatistics', () => { expect(summary.outputTokens).toBe(600); expect(summary.totalTokens).toBe(1800); }); + + it('should track thought and cached tokens', () => { + stats.recordTokens(100, 50, 10, 5); + + const summary = stats.getSummary(); + expect(summary.thoughtTokens).toBe(10); + expect(summary.cachedTokens).toBe(5); + expect(summary.totalTokens).toBe(165); // 100 + 50 + 10 + 5 + }); }); describe('tool usage statistics', () => { @@ -93,14 +102,14 @@ describe('SubagentStatistics', () => { stats.start(baseTime); stats.setRounds(2); stats.recordToolCall('file_read', true, 100); - stats.recordTokens(1000, 500); + stats.recordTokens(1000, 500, 20, 10); const result = stats.formatCompact('Test task', baseTime + 5000); expect(result).toContain('📋 Task Completed: Test task'); expect(result).toContain('🔧 Tool Usage: 1 calls, 100.0% success'); expect(result).toContain('⏱️ Duration: 5.0s | 🔁 Rounds: 2'); - expect(result).toContain('🔢 Tokens: 1,500 (in 1000, out 500)'); + expect(result).toContain('🔢 Tokens: 1,530 (in 1000, out 500)'); }); it('should handle zero tool calls', () => { diff --git a/packages/core/src/subagents/subagent-statistics.ts b/packages/core/src/subagents/subagent-statistics.ts index 3ef120c6..72308c63 100644 --- a/packages/core/src/subagents/subagent-statistics.ts +++ b/packages/core/src/subagents/subagent-statistics.ts @@ -23,6 +23,8 @@ export interface SubagentStatsSummary { successRate: number; inputTokens: number; outputTokens: number; + thoughtTokens: number; + cachedTokens: number; totalTokens: number; estimatedCost: number; toolUsage: ToolUsageStats[]; @@ -36,6 +38,8 @@ export class SubagentStatistics { private failedToolCalls = 0; private inputTokens = 0; private outputTokens = 0; + private thoughtTokens = 0; + private cachedTokens = 0; private toolUsage = new Map(); start(now = Date.now()) { @@ -74,9 +78,16 @@ export class SubagentStatistics { this.toolUsage.set(name, tu); } - recordTokens(input: number, output: number) { + recordTokens( + input: number, + output: number, + thought: number = 0, + cached: number = 0, + ) { this.inputTokens += Math.max(0, input || 0); this.outputTokens += Math.max(0, output || 0); + this.thoughtTokens += Math.max(0, thought || 0); + this.cachedTokens += Math.max(0, cached || 0); } getSummary(now = Date.now()): SubagentStatsSummary { @@ -86,7 +97,11 @@ export class SubagentStatistics { totalToolCalls > 0 ? (this.successfulToolCalls / totalToolCalls) * 100 : 0; - const totalTokens = this.inputTokens + this.outputTokens; + const totalTokens = + this.inputTokens + + this.outputTokens + + this.thoughtTokens + + this.cachedTokens; const estimatedCost = this.inputTokens * 3e-5 + this.outputTokens * 6e-5; return { rounds: this.rounds, @@ -97,6 +112,8 @@ export class SubagentStatistics { successRate, inputTokens: this.inputTokens, outputTokens: this.outputTokens, + thoughtTokens: this.thoughtTokens, + cachedTokens: this.cachedTokens, totalTokens, estimatedCost, toolUsage: Array.from(this.toolUsage.values()), @@ -116,8 +133,12 @@ export class SubagentStatistics { `⏱️ Duration: ${this.fmtDuration(stats.totalDurationMs)} | 🔁 Rounds: ${stats.rounds}`, ]; if (typeof stats.totalTokens === 'number') { + const parts = [ + `in ${stats.inputTokens ?? 0}`, + `out ${stats.outputTokens ?? 0}`, + ]; lines.push( - `🔢 Tokens: ${stats.totalTokens.toLocaleString()}${stats.inputTokens || stats.outputTokens ? ` (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})` : ''}`, + `🔢 Tokens: ${stats.totalTokens.toLocaleString()}${parts.length ? ` (${parts.join(', ')})` : ''}`, ); } return lines.join('\n'); @@ -152,8 +173,12 @@ export class SubagentStatistics { `🔧 Tools: ${stats.totalToolCalls} calls, ${sr.toFixed(1)}% success (${stats.successfulToolCalls} ok, ${stats.failedToolCalls} failed)`, ); if (typeof stats.totalTokens === 'number') { + const parts = [ + `in ${stats.inputTokens ?? 0}`, + `out ${stats.outputTokens ?? 0}`, + ]; lines.push( - `🔢 Tokens: ${stats.totalTokens.toLocaleString()} (in ${stats.inputTokens ?? 0}, out ${stats.outputTokens ?? 0})`, + `🔢 Tokens: ${stats.totalTokens.toLocaleString()} (${parts.join(', ')})`, ); } if (stats.toolUsage && stats.toolUsage.length) { diff --git a/packages/core/src/subagents/subagent.test.ts b/packages/core/src/subagents/subagent.test.ts index 256fb44d..742813cd 100644 --- a/packages/core/src/subagents/subagent.test.ts +++ b/packages/core/src/subagents/subagent.test.ts @@ -69,6 +69,8 @@ async function createMockConfig( targetDir: '.', debugMode: false, cwd: process.cwd(), + // Avoid writing any chat recording records from tests (e.g. via tool-call telemetry). + chatRecording: false, }; const config = new Config(configParams); await config.initialize(); diff --git a/packages/core/src/subagents/subagent.ts b/packages/core/src/subagents/subagent.ts index 885e8ca6..39e43e54 100644 --- a/packages/core/src/subagents/subagent.ts +++ b/packages/core/src/subagents/subagent.ts @@ -41,6 +41,7 @@ import type { SubAgentToolResultEvent, SubAgentStreamTextEvent, SubAgentErrorEvent, + SubAgentUsageEvent, } from './subagent-events.js'; import { type SubAgentEventEmitter, @@ -369,6 +370,7 @@ export class SubAgentScope { }, }; + const roundStreamStart = Date.now(); const responseStream = await chat.sendMessageStream( this.modelConfig.model || this.runtimeContext.getModel() || @@ -439,10 +441,19 @@ export class SubAgentScope { if (lastUsage) { const inTok = Number(lastUsage.promptTokenCount || 0); const outTok = Number(lastUsage.candidatesTokenCount || 0); - if (isFinite(inTok) || isFinite(outTok)) { + const thoughtTok = Number(lastUsage.thoughtsTokenCount || 0); + const cachedTok = Number(lastUsage.cachedContentTokenCount || 0); + if ( + isFinite(inTok) || + isFinite(outTok) || + isFinite(thoughtTok) || + isFinite(cachedTok) + ) { this.stats.recordTokens( isFinite(inTok) ? inTok : 0, isFinite(outTok) ? outTok : 0, + isFinite(thoughtTok) ? thoughtTok : 0, + isFinite(cachedTok) ? cachedTok : 0, ); // mirror legacy fields for compatibility this.executionStats.inputTokens = @@ -453,11 +464,20 @@ export class SubAgentScope { (isFinite(outTok) ? outTok : 0); this.executionStats.totalTokens = (this.executionStats.inputTokens || 0) + - (this.executionStats.outputTokens || 0); + (this.executionStats.outputTokens || 0) + + (isFinite(thoughtTok) ? thoughtTok : 0) + + (isFinite(cachedTok) ? cachedTok : 0); this.executionStats.estimatedCost = (this.executionStats.inputTokens || 0) * 3e-5 + (this.executionStats.outputTokens || 0) * 6e-5; } + this.eventEmitter?.emit(SubAgentEventType.USAGE_METADATA, { + subagentId: this.subagentId, + round: turnCounter, + usage: lastUsage, + durationMs: Date.now() - roundStreamStart, + timestamp: Date.now(), + } as SubAgentUsageEvent); } if (functionCalls.length > 0) { diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts index b6a97a2e..f0fb94f1 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts @@ -249,6 +249,9 @@ export class QwenLogger { authType === AuthType.USE_OPENAI ? this.config?.getContentGeneratorConfig().baseUrl || '' : '', + ...(this.config?.getChannel?.() + ? { channel: this.config.getChannel() } + : {}), }, _v: `qwen-code@${version}`, } as RumPayload; diff --git a/packages/core/src/telemetry/uiTelemetry.ts b/packages/core/src/telemetry/uiTelemetry.ts index 9a257e5a..0f8f2146 100644 --- a/packages/core/src/telemetry/uiTelemetry.ts +++ b/packages/core/src/telemetry/uiTelemetry.ts @@ -23,6 +23,12 @@ export type UiEvent = | (ApiErrorEvent & { 'event.name': typeof EVENT_API_ERROR }) | (ToolCallEvent & { 'event.name': typeof EVENT_TOOL_CALL }); +export { + EVENT_API_ERROR, + EVENT_API_RESPONSE, + EVENT_TOOL_CALL, +} from './constants.js'; + export interface ToolCallStats { count: number; success: number; diff --git a/packages/core/src/tools/glob.test.ts b/packages/core/src/tools/glob.test.ts index 3729c251..b6a04c35 100644 --- a/packages/core/src/tools/glob.test.ts +++ b/packages/core/src/tools/glob.test.ts @@ -198,6 +198,52 @@ describe('GlobTool', () => { ); }); + it('should find files even if workspace path casing differs from glob results (Windows/macOS)', async () => { + // Only relevant for Windows and macOS + if (process.platform !== 'win32' && process.platform !== 'darwin') { + return; + } + + let mismatchedRootDir = tempRootDir; + + if (process.platform === 'win32') { + // 1. Create a path with mismatched casing for the workspace root + // e.g., if tempRootDir is "C:\Users\...", make it "c:\Users\..." + const drive = path.parse(tempRootDir).root; + if (!drive || !drive.match(/^[A-Z]:\\/)) { + // Skip if we can't determine/manipulate the drive letter easily + return; + } + + const lowerDrive = drive.toLowerCase(); + mismatchedRootDir = lowerDrive + tempRootDir.substring(drive.length); + } else { + // macOS: change the casing of the path + if (tempRootDir === tempRootDir.toLowerCase()) { + mismatchedRootDir = tempRootDir.toUpperCase(); + } else { + mismatchedRootDir = tempRootDir.toLowerCase(); + } + } + + // 2. Create a new GlobTool instance with this mismatched root + const mismatchedConfig = { + ...mockConfig, + getTargetDir: () => mismatchedRootDir, + getWorkspaceContext: () => + createMockWorkspaceContext(mismatchedRootDir), + } as unknown as Config; + + const mismatchedGlobTool = new GlobTool(mismatchedConfig); + + // 3. Execute search + const params: GlobToolParams = { pattern: '*.txt' }; + const invocation = mismatchedGlobTool.build(params); + const result = await invocation.execute(abortSignal); + + expect(result.llmContent).toContain('Found 2 file(s)'); + }); + it('should return error if path is outside workspace', async () => { // Bypassing validation to test execute method directly vi.spyOn(globTool, 'validateToolParams').mockReturnValue(null); diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts index 29b6cf86..a3b4a5d5 100644 --- a/packages/core/src/tools/glob.ts +++ b/packages/core/src/tools/glob.ts @@ -134,12 +134,21 @@ class GlobToolInvocation extends BaseToolInvocation< this.getFileFilteringOptions(), ); + const normalizePathForComparison = (p: string) => + process.platform === 'win32' || process.platform === 'darwin' + ? p.toLowerCase() + : p; + const filteredAbsolutePaths = new Set( - filteredPaths.map((p) => path.resolve(this.config.getTargetDir(), p)), + filteredPaths.map((p) => + normalizePathForComparison( + path.resolve(this.config.getTargetDir(), p), + ), + ), ); const filteredEntries = allEntries.filter((entry) => - filteredAbsolutePaths.has(entry.fullpath()), + filteredAbsolutePaths.has(normalizePathForComparison(entry.fullpath())), ); if (!filteredEntries || filteredEntries.length === 0) { diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json index b0f35709..b071b8a3 100644 --- a/packages/sdk-typescript/package.json +++ b/packages/sdk-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/sdk", - "version": "0.1.0", + "version": "0.5.1", "description": "TypeScript SDK for programmatic access to qwen-code CLI", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk-typescript/src/transport/ProcessTransport.ts b/packages/sdk-typescript/src/transport/ProcessTransport.ts index c54d9104..43ff09da 100644 --- a/packages/sdk-typescript/src/transport/ProcessTransport.ts +++ b/packages/sdk-typescript/src/transport/ProcessTransport.ts @@ -139,6 +139,7 @@ export class ProcessTransport implements Transport { 'stream-json', '--output-format', 'stream-json', + '--channel=SDK', ]; if (this.options.model) { diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 7365c059..d7d32ac9 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.4.1", + "version": "0.5.1", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/.vscodeignore b/packages/vscode-ide-companion/.vscodeignore index e74d0536..18e07a04 100644 --- a/packages/vscode-ide-companion/.vscodeignore +++ b/packages/vscode-ide-companion/.vscodeignore @@ -1,5 +1,6 @@ ** !dist/ +!dist/** ../ ../../ !LICENSE diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index c278976f..faffc3f5 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -2,7 +2,7 @@ "name": "qwen-code-vscode-ide-companion", "displayName": "Qwen Code Companion", "description": "Enable Qwen Code with direct access to your VS Code workspace.", - "version": "0.4.1", + "version": "0.5.1", "publisher": "qwenlm", "icon": "assets/icon.png", "repository": { @@ -113,7 +113,7 @@ "main": "./dist/extension.cjs", "type": "module", "scripts": { - "prepackage": "npm run generate:notices && npm run check-types && npm run lint && npm run build:prod", + "prepackage": "node ./scripts/prepackage.js", "build": "npm run build:dev", "build:dev": "npm run check-types && npm run lint && node esbuild.js", "build:prod": "node esbuild.js --production", diff --git a/packages/vscode-ide-companion/scripts/copy-bundled-cli.js b/packages/vscode-ide-companion/scripts/copy-bundled-cli.js new file mode 100644 index 00000000..d720e47f --- /dev/null +++ b/packages/vscode-ide-companion/scripts/copy-bundled-cli.js @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Copy the already-built root dist/ folder into the extension dist/qwen-cli/. + * + * Assumes repoRoot/dist already exists (e.g. produced by `npm run bundle` and + * optionally `npm run prepare:package`). + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs/promises'; +import { existsSync } from 'node:fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const extensionRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(extensionRoot, '..', '..'); +const rootDistDir = path.join(repoRoot, 'dist'); +const extensionDistDir = path.join(extensionRoot, 'dist'); +const bundledCliDir = path.join(extensionDistDir, 'qwen-cli'); + +async function main() { + const cliJs = path.join(rootDistDir, 'cli.js'); + const vendorDir = path.join(rootDistDir, 'vendor'); + + if (!existsSync(cliJs) || !existsSync(vendorDir)) { + throw new Error( + `[copy-bundled-cli] Missing root dist artifacts. Expected:\n- ${cliJs}\n- ${vendorDir}\n\nRun root "npm run bundle" first.`, + ); + } + + await fs.mkdir(extensionDistDir, { recursive: true }); + const existingNodeModules = path.join(bundledCliDir, 'node_modules'); + const tmpNodeModules = path.join( + extensionDistDir, + 'qwen-cli.node_modules.tmp', + ); + const keepNodeModules = existsSync(existingNodeModules); + + // Preserve destination node_modules if it exists (e.g. after packaging install). + if (keepNodeModules) { + await fs.rm(tmpNodeModules, { recursive: true, force: true }); + await fs.rename(existingNodeModules, tmpNodeModules); + } + + await fs.rm(bundledCliDir, { recursive: true, force: true }); + await fs.mkdir(bundledCliDir, { recursive: true }); + + await fs.cp(rootDistDir, bundledCliDir, { recursive: true }); + + if (keepNodeModules) { + await fs.rename(tmpNodeModules, existingNodeModules); + } + + console.log(`[copy-bundled-cli] Copied ${rootDistDir} -> ${bundledCliDir}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/vscode-ide-companion/scripts/prepackage.js b/packages/vscode-ide-companion/scripts/prepackage.js new file mode 100644 index 00000000..8db18a69 --- /dev/null +++ b/packages/vscode-ide-companion/scripts/prepackage.js @@ -0,0 +1,98 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * VS Code extension packaging orchestration. + * + * We bundle the CLI into the extension so users don't need a global install. + * To match the published CLI layout, we need to: + * - build root bundle (dist/cli.js + vendor/ + sandbox profiles) + * - run root prepare:package (dist/package.json + locales + README/LICENSE) + * - install production deps into root dist/ (dist/node_modules) so runtime deps + * like optional node-pty are present inside the VSIX payload. + * + * Then we generate notices and build the extension. + */ + +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const extensionRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(extensionRoot, '..', '..'); +const bundledCliDir = path.join(extensionRoot, 'dist', 'qwen-cli'); + +function npmBin() { + return process.platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +function run(cmd, args, opts = {}) { + const res = spawnSync(cmd, args, { + stdio: 'inherit', + shell: process.platform === 'win32' ? true : false, + ...opts, + }); + if (res.error) { + throw res.error; + } + if (typeof res.status === 'number' && res.status !== 0) { + throw new Error( + `Command failed (${res.status}): ${cmd} ${args.map((a) => JSON.stringify(a)).join(' ')}`, + ); + } +} + +function main() { + const npm = npmBin(); + + console.log('[prepackage] Bundling root CLI...'); + run(npm, ['--prefix', repoRoot, 'run', 'bundle'], { cwd: repoRoot }); + + console.log('[prepackage] Preparing root dist/ package metadata...'); + run(npm, ['--prefix', repoRoot, 'run', 'prepare:package'], { cwd: repoRoot }); + + console.log('[prepackage] Generating notices...'); + run(npm, ['run', 'generate:notices'], { cwd: extensionRoot }); + + console.log('[prepackage] Typechecking...'); + run(npm, ['run', 'check-types'], { cwd: extensionRoot }); + + console.log('[prepackage] Linting...'); + run(npm, ['run', 'lint'], { cwd: extensionRoot }); + + console.log('[prepackage] Building extension (production)...'); + run(npm, ['run', 'build:prod'], { cwd: extensionRoot }); + + console.log('[prepackage] Copying bundled CLI dist/ into extension...'); + run( + 'node', + [`${path.join(extensionRoot, 'scripts', 'copy-bundled-cli.js')}`], + { + cwd: extensionRoot, + }, + ); + + console.log( + '[prepackage] Installing production deps into extension dist/qwen-cli...', + ); + run( + npm, + [ + '--prefix', + bundledCliDir, + 'install', + '--omit=dev', + '--no-audit', + '--no-fund', + ], + { cwd: bundledCliDir }, + ); +} + +main(); diff --git a/packages/vscode-ide-companion/src/cli/cliContextManager.ts b/packages/vscode-ide-companion/src/cli/cliContextManager.ts deleted file mode 100644 index c812a08e..00000000 --- a/packages/vscode-ide-companion/src/cli/cliContextManager.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { CliFeatureFlags, CliVersionInfo } from './cliVersionManager.js'; - -export class CliContextManager { - private static instance: CliContextManager; - private currentVersionInfo: CliVersionInfo | null = null; - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): CliContextManager { - if (!CliContextManager.instance) { - CliContextManager.instance = new CliContextManager(); - } - return CliContextManager.instance; - } - - /** - * Set current CLI version information - * - * @param versionInfo - CLI version information - */ - setCurrentVersionInfo(versionInfo: CliVersionInfo): void { - this.currentVersionInfo = versionInfo; - } - - /** - * Get current CLI feature flags - * - * @returns Current CLI feature flags or default flags if not set - */ - getCurrentFeatures(): CliFeatureFlags { - if (this.currentVersionInfo) { - return this.currentVersionInfo.features; - } - - // Return default feature flags (all disabled) - return { - supportsSessionList: false, - supportsSessionLoad: false, - }; - } - - supportsSessionList(): boolean { - return this.getCurrentFeatures().supportsSessionList; - } - - supportsSessionLoad(): boolean { - return this.getCurrentFeatures().supportsSessionLoad; - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliDetector.ts b/packages/vscode-ide-companion/src/cli/cliDetector.ts deleted file mode 100644 index 875c2858..00000000 --- a/packages/vscode-ide-companion/src/cli/cliDetector.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import { exec } from 'child_process'; -import { promisify } from 'util'; - -const execAsync = promisify(exec); - -export interface CliDetectionResult { - isInstalled: boolean; - cliPath?: string; - version?: string; - error?: string; -} - -/** - * Detects if Qwen Code CLI is installed and accessible - */ -export class CliDetector { - private static cachedResult: CliDetectionResult | null = null; - private static lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - /** - * Checks if the Qwen Code CLI is installed - * @param forceRefresh - Force a new check, ignoring cache - * @returns Detection result with installation status and details - */ - static async detectQwenCli( - forceRefresh = false, - ): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedResult && - now - this.lastCheckTime < this.CACHE_DURATION_MS - ) { - console.log('[CliDetector] Returning cached result'); - return this.cachedResult; - } - - console.log( - '[CliDetector] Starting CLI detection, current PATH:', - process.env.PATH, - ); - - try { - const isWindows = process.platform === 'win32'; - const whichCommand = isWindows ? 'where' : 'which'; - - // Check if qwen command exists - try { - // Use NVM environment for consistent detection - // Fallback chain: default alias -> node alias -> current version - const detectionCommand = - process.platform === 'win32' - ? `${whichCommand} qwen` - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); which qwen'; - - console.log( - '[CliDetector] Detecting CLI with command:', - detectionCommand, - ); - - const { stdout } = await execAsync(detectionCommand, { - timeout: 5000, - shell: '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual path - const lines = stdout - .trim() - .split('\n') - .filter((line) => line.trim()); - const cliPath = lines[lines.length - 1]; - - console.log('[CliDetector] Found CLI at:', cliPath); - - // Try to get version - let version: string | undefined; - try { - // Use NVM environment for version check - // Fallback chain: default alias -> node alias -> current version - // Also ensure we use the correct Node.js version that matches the CLI installation - const versionCommand = - process.platform === 'win32' - ? 'qwen --version' - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); qwen --version'; - - console.log( - '[CliDetector] Getting version with command:', - versionCommand, - ); - - const { stdout: versionOutput } = await execAsync(versionCommand, { - timeout: 5000, - shell: '/bin/bash', - }); - // The output may contain multiple lines, with NVM activation messages - // We want the last line which should be the actual version - const versionLines = versionOutput - .trim() - .split('\n') - .filter((line) => line.trim()); - version = versionLines[versionLines.length - 1]; - console.log('[CliDetector] CLI version:', version); - } catch (versionError) { - console.log('[CliDetector] Failed to get CLI version:', versionError); - // Version check failed, but CLI is installed - } - - this.cachedResult = { - isInstalled: true, - cliPath, - version, - }; - this.lastCheckTime = now; - return this.cachedResult; - } catch (detectionError) { - console.log('[CliDetector] CLI not found, error:', detectionError); - // CLI not found - let error = `Qwen Code CLI not found in PATH. Please install it using: npm install -g @qwen-code/qwen-code@latest`; - - // Provide specific guidance for permission errors - if (detectionError instanceof Error) { - const errorMessage = detectionError.message; - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - error += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - } - - this.cachedResult = { - isInstalled: false, - error, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } catch (error) { - console.log('[CliDetector] General detection error:', error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - let userFriendlyError = `Failed to detect Qwen Code CLI: ${errorMessage}`; - - // Provide specific guidance for permission errors - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyError += `\n\nThis may be due to permission issues. Possible solutions: - \n1. Reinstall the CLI without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. If previously installed with sudo, fix ownership: sudo chown -R $(whoami) $(npm config get prefix)/lib/node_modules/@qwen-code/qwen-code - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Check your PATH environment variable includes npm's global bin directory`; - } - - this.cachedResult = { - isInstalled: false, - error: userFriendlyError, - }; - this.lastCheckTime = now; - return this.cachedResult; - } - } - - /** - * Clears the cached detection result - */ - static clearCache(): void { - this.cachedResult = null; - this.lastCheckTime = 0; - } - - /** - * Gets installation instructions based on the platform - */ - static getInstallationInstructions(): { - title: string; - steps: string[]; - documentationUrl: string; - } { - return { - title: 'Qwen Code CLI is not installed', - steps: [ - 'Install via npm:', - ' npm install -g @qwen-code/qwen-code@latest', - '', - 'If you are using nvm (automatically handled by the plugin):', - ' The plugin will automatically use your default nvm version', - '', - 'Or install from source:', - ' git clone https://github.com/QwenLM/qwen-code.git', - ' cd qwen-code', - ' npm install', - ' npm install -g .', - '', - 'After installation, reload VS Code or restart the extension.', - ], - documentationUrl: 'https://github.com/QwenLM/qwen-code#installation', - }; - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliInstaller.ts b/packages/vscode-ide-companion/src/cli/cliInstaller.ts deleted file mode 100644 index 4eb0d0e7..00000000 --- a/packages/vscode-ide-companion/src/cli/cliInstaller.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { CliDetector } from './cliDetector.js'; - -/** - * CLI Detection and Installation Handler - * Responsible for detecting, installing, and prompting for Qwen CLI - */ -export class CliInstaller { - /** - * Check CLI installation status and send results to WebView - * @param sendToWebView Callback function to send messages to WebView - */ - static async checkInstallation( - sendToWebView: (message: unknown) => void, - ): Promise { - try { - const result = await CliDetector.detectQwenCli(); - - sendToWebView({ - type: 'cliDetectionResult', - data: { - isInstalled: result.isInstalled, - cliPath: result.cliPath, - version: result.version, - error: result.error, - installInstructions: result.isInstalled - ? undefined - : CliDetector.getInstallationInstructions(), - }, - }); - - if (!result.isInstalled) { - console.log('[CliInstaller] Qwen CLI not detected:', result.error); - } else { - console.log( - '[CliInstaller] Qwen CLI detected:', - result.cliPath, - result.version, - ); - } - } catch (error) { - console.error('[CliInstaller] CLI detection error:', error); - } - } - - /** - * Prompt user to install CLI - * Display warning message with installation options - */ - static async promptInstallation(): Promise { - const selection = await vscode.window.showWarningMessage( - 'Qwen Code CLI is not installed. You can browse conversation history, but cannot send new messages.', - 'Install Now', - 'View Documentation', - 'Remind Me Later', - ); - - if (selection === 'Install Now') { - await this.install(); - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse('https://github.com/QwenLM/qwen-code#installation'), - ); - } - } - - /** - * Install Qwen CLI - * Install global CLI package via npm - */ - static async install(): Promise { - try { - // Show progress notification - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: 'Installing Qwen Code CLI', - cancellable: false, - }, - async (progress) => { - progress.report({ - message: 'Running: npm install -g @qwen-code/qwen-code@latest', - }); - - const { exec } = await import('child_process'); - const { promisify } = await import('util'); - const execAsync = promisify(exec); - - try { - // Use NVM environment to ensure we get the same Node.js version - // as when they run 'node -v' in terminal - // Fallback chain: default alias -> node alias -> current version - const installCommand = - process.platform === 'win32' - ? 'npm install -g @qwen-code/qwen-code@latest' - : 'source ~/.nvm/nvm.sh 2>/dev/null && (nvm use default 2>/dev/null || nvm use node 2>/dev/null || nvm use 2>/dev/null); npm install -g @qwen-code/qwen-code@latest'; - - console.log( - '[CliInstaller] Installing with command:', - installCommand, - ); - console.log( - '[CliInstaller] Current process PATH:', - process.env['PATH'], - ); - - // Also log Node.js version being used by VS Code - console.log( - '[CliInstaller] VS Code Node.js version:', - process.version, - ); - console.log( - '[CliInstaller] VS Code Node.js execPath:', - process.execPath, - ); - - const { stdout, stderr } = await execAsync( - installCommand, - { - timeout: 120000, - shell: '/bin/bash', - }, // 2 minutes timeout - ); - - console.log('[CliInstaller] Installation output:', stdout); - if (stderr) { - console.warn('[CliInstaller] Installation stderr:', stderr); - } - - // Clear cache and recheck - CliDetector.clearCache(); - const detection = await CliDetector.detectQwenCli(); - - if (detection.isInstalled) { - vscode.window - .showInformationMessage( - `✅ Qwen Code CLI installed successfully! Version: ${detection.version}`, - 'Reload Window', - ) - .then((selection) => { - if (selection === 'Reload Window') { - vscode.commands.executeCommand( - 'workbench.action.reloadWindow', - ); - } - }); - } else { - throw new Error( - 'Installation completed but CLI still not detected', - ); - } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error('[CliInstaller] Installation failed:', errorMessage); - console.error('[CliInstaller] Error stack:', error); - - // Provide specific guidance for permission errors - let userFriendlyMessage = `Failed to install Qwen Code CLI: ${errorMessage}`; - - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - userFriendlyMessage += `\n\nThis is likely due to permission issues. Possible solutions: - \n1. Reinstall without sudo: npm install -g @qwen-code/qwen-code@latest - \n2. Fix npm permissions: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} - \n3. Use nvm for Node.js version management to avoid permission issues - \n4. Configure npm to use a different directory: npm config set prefix ~/.npm-global`; - } - - vscode.window - .showErrorMessage( - userFriendlyMessage, - 'Try Manual Installation', - 'View Documentation', - ) - .then((selection) => { - if (selection === 'Try Manual Installation') { - const terminal = vscode.window.createTerminal( - 'Qwen Code Installation', - ); - terminal.show(); - - // Provide different installation commands based on error type - if ( - errorMessage.includes('EACCES') || - errorMessage.includes('Permission denied') - ) { - terminal.sendText('# Try installing without sudo:'); - terminal.sendText( - 'npm install -g @qwen-code/qwen-code@latest', - ); - terminal.sendText(''); - terminal.sendText('# Or fix npm permissions:'); - terminal.sendText( - 'sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}', - ); - } else { - terminal.sendText( - 'npm install -g @qwen-code/qwen-code@latest', - ); - } - } else if (selection === 'View Documentation') { - vscode.env.openExternal( - vscode.Uri.parse( - 'https://github.com/QwenLM/qwen-code#installation', - ), - ); - } - }); - } - }, - ); - } catch (error) { - console.error('[CliInstaller] Install CLI error:', error); - } - } -} diff --git a/packages/vscode-ide-companion/src/cli/cliPathDetector.ts b/packages/vscode-ide-companion/src/cli/cliPathDetector.ts deleted file mode 100644 index 7f329873..00000000 --- a/packages/vscode-ide-companion/src/cli/cliPathDetector.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import { statSync } from 'fs'; - -export interface CliPathDetectionResult { - path: string | null; - error?: string; -} - -/** - * Determine the correct Node.js executable path for a given CLI installation - * Handles various Node.js version managers (nvm, n, manual installations) - * - * @param cliPath - Path to the CLI executable - * @returns Path to the Node.js executable, or null if not found - */ -export function determineNodePathForCli( - cliPath: string, -): CliPathDetectionResult { - // Common patterns for Node.js installations - const nodePathPatterns = [ - // NVM pattern: /Users/user/.nvm/versions/node/vXX.XX.X/bin/qwen -> /Users/user/.nvm/versions/node/vXX.XX.X/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - - // N pattern: /Users/user/n/bin/qwen -> /Users/user/n/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - - // Manual installation pattern: /usr/local/bin/qwen -> /usr/local/bin/node - cliPath.replace(/\/qwen$/, '/node'), - - // Alternative pattern: /opt/nodejs/bin/qwen -> /opt/nodejs/bin/node - cliPath.replace(/\/bin\/qwen$/, '/bin/node'), - ]; - - // Check each pattern - for (const nodePath of nodePathPatterns) { - try { - const stats = statSync(nodePath); - if (stats.isFile()) { - // Verify it's executable - if (stats.mode & 0o111) { - console.log(`[CLI] Found Node.js executable for CLI at: ${nodePath}`); - return { path: nodePath }; - } else { - console.log(`[CLI] Node.js found at ${nodePath} but not executable`); - return { - path: null, - error: `Node.js found at ${nodePath} but not executable. You may need to fix file permissions or reinstall the CLI.`, - }; - } - } - } catch (error) { - // Differentiate between error types - if (error instanceof Error) { - if ('code' in error && error.code === 'EACCES') { - console.log(`[CLI] Permission denied accessing ${nodePath}`); - return { - path: null, - error: `Permission denied accessing ${nodePath}. The CLI may have been installed with sudo privileges. Try reinstalling without sudo or adjusting file permissions.`, - }; - } else if ('code' in error && error.code === 'ENOENT') { - // File not found, continue to next pattern - continue; - } else { - console.log(`[CLI] Error accessing ${nodePath}: ${error.message}`); - return { - path: null, - error: `Error accessing Node.js at ${nodePath}: ${error.message}`, - }; - } - } - } - } - - // Try to find node in the same directory as the CLI - const cliDir = cliPath.substring(0, cliPath.lastIndexOf('/')); - const potentialNodePaths = [`${cliDir}/node`, `${cliDir}/bin/node`]; - - for (const nodePath of potentialNodePaths) { - try { - const stats = statSync(nodePath); - if (stats.isFile()) { - if (stats.mode & 0o111) { - console.log( - `[CLI] Found Node.js executable in CLI directory at: ${nodePath}`, - ); - return { path: nodePath }; - } else { - console.log(`[CLI] Node.js found at ${nodePath} but not executable`); - return { - path: null, - error: `Node.js found at ${nodePath} but not executable. You may need to fix file permissions or reinstall the CLI.`, - }; - } - } - } catch (error) { - // Differentiate between error types - if (error instanceof Error) { - if ('code' in error && error.code === 'EACCES') { - console.log(`[CLI] Permission denied accessing ${nodePath}`); - return { - path: null, - error: `Permission denied accessing ${nodePath}. The CLI may have been installed with sudo privileges. Try reinstalling without sudo or adjusting file permissions.`, - }; - } else if ('code' in error && error.code === 'ENOENT') { - // File not found, continue - continue; - } else { - console.log(`[CLI] Error accessing ${nodePath}: ${error.message}`); - return { - path: null, - error: `Error accessing Node.js at ${nodePath}: ${error.message}`, - }; - } - } - } - } - - console.log(`[CLI] Could not determine Node.js path for CLI: ${cliPath}`); - return { - path: null, - error: `Could not find Node.js executable for CLI at ${cliPath}. Please verify the CLI installation.`, - }; -} diff --git a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts b/packages/vscode-ide-companion/src/cli/cliVersionManager.ts deleted file mode 100644 index 72ef3d2e..00000000 --- a/packages/vscode-ide-companion/src/cli/cliVersionManager.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import semver from 'semver'; -import { CliDetector, type CliDetectionResult } from './cliDetector.js'; - -export const MIN_CLI_VERSION_FOR_SESSION_METHODS = '0.4.0'; - -export interface CliFeatureFlags { - supportsSessionList: boolean; - supportsSessionLoad: boolean; -} - -export interface CliVersionInfo { - version: string | undefined; - isSupported: boolean; - features: CliFeatureFlags; - detectionResult: CliDetectionResult; -} - -/** - * CLI Version Manager - * - * Manages CLI version detection and feature availability based on version - */ -export class CliVersionManager { - private static instance: CliVersionManager; - private cachedVersionInfo: CliVersionInfo | null = null; - private lastCheckTime: number = 0; - private static readonly CACHE_DURATION_MS = 30000; // 30 seconds - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): CliVersionManager { - if (!CliVersionManager.instance) { - CliVersionManager.instance = new CliVersionManager(); - } - return CliVersionManager.instance; - } - - /** - * Check if CLI version meets minimum requirements - * - * @param version - Version string to check - * @param minVersion - Minimum required version - * @returns Whether version meets requirements - */ - private isVersionSupported( - version: string | undefined, - minVersion: string, - ): boolean { - if (!version) { - return false; - } - - // Use semver for robust comparison (handles v-prefix, pre-release, etc.) - const v = semver.valid(version) ?? semver.coerce(version)?.version ?? null; - const min = - semver.valid(minVersion) ?? semver.coerce(minVersion)?.version ?? null; - - if (!v || !min) { - console.warn( - `[CliVersionManager] Invalid semver: version=${version}, min=${minVersion}`, - ); - return false; - } - console.log(`[CliVersionManager] Version ${v} meets requirements: ${min}`); - return semver.gte(v, min); - } - - /** - * Get feature flags based on CLI version - * - * @param version - CLI version string - * @returns Feature flags - */ - private getFeatureFlags(version: string | undefined): CliFeatureFlags { - const isSupportedVersion = this.isVersionSupported( - version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ); - - return { - supportsSessionList: isSupportedVersion, - supportsSessionLoad: isSupportedVersion, - }; - } - - /** - * Detect CLI version and features - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns CLI version information - */ - async detectCliVersion(forceRefresh = false): Promise { - const now = Date.now(); - - // Return cached result if available and not expired - if ( - !forceRefresh && - this.cachedVersionInfo && - now - this.lastCheckTime < CliVersionManager.CACHE_DURATION_MS - ) { - console.log('[CliVersionManager] Returning cached version info'); - return this.cachedVersionInfo; - } - - console.log('[CliVersionManager] Detecting CLI version...'); - - try { - // Detect CLI installation - const detectionResult = await CliDetector.detectQwenCli(forceRefresh); - - const versionInfo: CliVersionInfo = { - version: detectionResult.version, - isSupported: this.isVersionSupported( - detectionResult.version, - MIN_CLI_VERSION_FOR_SESSION_METHODS, - ), - features: this.getFeatureFlags(detectionResult.version), - detectionResult, - }; - - // Cache the result - this.cachedVersionInfo = versionInfo; - this.lastCheckTime = now; - - console.log( - '[CliVersionManager] CLI version detection result:', - versionInfo, - ); - - return versionInfo; - } catch (error) { - console.error('[CliVersionManager] Failed to detect CLI version:', error); - - // Return fallback result - const fallbackResult: CliVersionInfo = { - version: undefined, - isSupported: false, - features: { - supportsSessionList: false, - supportsSessionLoad: false, - }, - detectionResult: { - isInstalled: false, - error: error instanceof Error ? error.message : String(error), - }, - }; - - return fallbackResult; - } - } - - /** - * Clear cached version information - */ - clearCache(): void { - this.cachedVersionInfo = null; - this.lastCheckTime = 0; - CliDetector.clearCache(); - } - - /** - * Check if CLI supports session/list method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/list is supported - */ - async supportsSessionList(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionList; - } - - /** - * Check if CLI supports session/load method - * - * @param forceRefresh - Force a new check, ignoring cache - * @returns Whether session/load is supported - */ - async supportsSessionLoad(forceRefresh = false): Promise { - const versionInfo = await this.detectCliVersion(forceRefresh); - return versionInfo.features.supportsSessionLoad; - } -} diff --git a/packages/vscode-ide-companion/src/constants/acpSchema.ts b/packages/vscode-ide-companion/src/constants/acpSchema.ts index 18a69641..9f06e4fa 100644 --- a/packages/vscode-ide-companion/src/constants/acpSchema.ts +++ b/packages/vscode-ide-companion/src/constants/acpSchema.ts @@ -19,6 +19,7 @@ export const AGENT_METHODS = { export const CLIENT_METHODS = { fs_read_text_file: 'fs/read_text_file', fs_write_text_file: 'fs/write_text_file', + authenticate_update: 'authenticate/update', session_request_permission: 'session/request_permission', session_update: 'session/update', } as const; diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index 2adfaef1..c27a7e9d 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -292,7 +292,14 @@ export async function activate(context: vscode.ExtensionContext) { } if (selectedFolder) { - const qwenCmd = 'qwen'; + const cliEntry = vscode.Uri.joinPath( + context.extensionUri, + 'dist', + 'qwen-cli', + 'cli.js', + ).fsPath; + const quote = (s: string) => `"${s.replaceAll('"', '\\"')}"`; + const qwenCmd = `${quote(process.execPath)} ${quote(cliEntry)}`; const terminal = vscode.window.createTerminal({ name: `Qwen Code (${selectedFolder.name})`, cwd: selectedFolder.uri.fsPath, diff --git a/packages/vscode-ide-companion/src/ide-server.ts b/packages/vscode-ide-companion/src/ide-server.ts index 8324f802..69fabbc4 100644 --- a/packages/vscode-ide-companion/src/ide-server.ts +++ b/packages/vscode-ide-companion/src/ide-server.ts @@ -164,6 +164,7 @@ export class IDEServer { const allowedHosts = [ `localhost:${this.port}`, `127.0.0.1:${this.port}`, + `host.docker.internal:${this.port}`, // Add Docker support ]; if (!allowedHosts.includes(host)) { return res.status(403).json({ error: 'Invalid Host header' }); diff --git a/packages/vscode-ide-companion/src/services/acpConnection.ts b/packages/vscode-ide-companion/src/services/acpConnection.ts index 5486e14d..4b2c4028 100644 --- a/packages/vscode-ide-companion/src/services/acpConnection.ts +++ b/packages/vscode-ide-companion/src/services/acpConnection.ts @@ -10,8 +10,9 @@ import type { AcpPermissionRequest, AcpResponse, AcpSessionUpdate, - ApprovalModeValue, + AuthenticateUpdateNotification, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { ChildProcess, SpawnOptions } from 'child_process'; import { spawn } from 'child_process'; import type { @@ -20,7 +21,7 @@ import type { } from '../types/connectionTypes.js'; import { AcpMessageHandler } from './acpMessageHandler.js'; import { AcpSessionManager } from './acpSessionManager.js'; -import { determineNodePathForCli } from '../cli/cliPathDetector.js'; +import * as fs from 'node:fs'; /** * ACP Connection Handler for VSCode Extension @@ -42,6 +43,8 @@ export class AcpConnection { onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ optionId: string; }> = () => Promise.resolve({ optionId: 'allow' }); + onAuthenticateUpdate: (data: AuthenticateUpdateNotification) => void = + () => {}; onEndTurn: () => void = () => {}; // Called after successful initialize() with the initialize result onInitialized: (init: unknown) => void = () => {}; @@ -54,12 +57,12 @@ export class AcpConnection { /** * Connect to Qwen ACP * - * @param cliPath - CLI path + * @param cliEntryPath - Path to the bundled CLI entrypoint (cli.js) * @param workingDir - Working directory * @param extraArgs - Extra command line arguments */ async connect( - cliPath: string, + cliEntryPath: string, workingDir: string = process.cwd(), extraArgs: string[] = [], ): Promise { @@ -69,7 +72,6 @@ export class AcpConnection { this.workingDir = workingDir; - const isWindows = process.platform === 'win32'; const env = { ...process.env }; // If proxy is configured in extraArgs, also set it as environment variable @@ -88,38 +90,20 @@ export class AcpConnection { env['https_proxy'] = proxyUrl; } - let spawnCommand: string; - let spawnArgs: string[]; + // Always run the bundled CLI using the VS Code extension host's Node runtime. + // This avoids PATH/NVM/global install problems and ensures deterministic behavior. + const spawnCommand: string = process.execPath; + const spawnArgs: string[] = [ + cliEntryPath, + '--experimental-acp', + '--channel=VSCode', + ...extraArgs, + ]; - if (cliPath.startsWith('npx ')) { - const parts = cliPath.split(' '); - spawnCommand = isWindows ? 'npx.cmd' : 'npx'; - spawnArgs = [...parts.slice(1), '--experimental-acp', ...extraArgs]; - } else { - // For qwen CLI, ensure we use the correct Node.js version - // Handle various Node.js version managers (nvm, n, manual installations) - if (cliPath.includes('/qwen') && !isWindows) { - // Try to determine the correct node executable for this qwen installation - const nodePathResult = determineNodePathForCli(cliPath); - if (nodePathResult.path) { - spawnCommand = nodePathResult.path; - spawnArgs = [cliPath, '--experimental-acp', ...extraArgs]; - } else { - // Fallback to direct execution - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', ...extraArgs]; - - // Log any error for debugging - if (nodePathResult.error) { - console.warn( - `[ACP] Node.js path detection warning: ${nodePathResult.error}`, - ); - } - } - } else { - spawnCommand = cliPath; - spawnArgs = ['--experimental-acp', ...extraArgs]; - } + if (!fs.existsSync(cliEntryPath)) { + throw new Error( + `Bundled Qwen CLI entry not found at ${cliEntryPath}. The extension may not have been packaged correctly.`, + ); } console.log('[ACP] Spawning command:', spawnCommand, spawnArgs.join(' ')); @@ -128,7 +112,8 @@ export class AcpConnection { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], env, - shell: isWindows, + // We spawn node directly; no shell needed (and shell quoting can break paths). + shell: false, }; this.child = spawn(spawnCommand, spawnArgs, options); @@ -225,6 +210,7 @@ export class AcpConnection { const callbacks: AcpConnectionCallbacks = { onSessionUpdate: this.onSessionUpdate, onPermissionRequest: this.onPermissionRequest, + onAuthenticateUpdate: this.onAuthenticateUpdate, onEndTurn: this.onEndTurn, }; diff --git a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts index db7802ce..8766fdf3 100644 --- a/packages/vscode-ide-companion/src/services/acpMessageHandler.ts +++ b/packages/vscode-ide-companion/src/services/acpMessageHandler.ts @@ -17,6 +17,7 @@ import type { AcpResponse, AcpSessionUpdate, AcpPermissionRequest, + AuthenticateUpdateNotification, } from '../types/acpTypes.js'; import { CLIENT_METHODS } from '../constants/acpSchema.js'; import type { @@ -110,13 +111,20 @@ export class AcpMessageHandler { // JSON.stringify(message.result).substring(0, 200), message.result, ); - if ( - message.result && - typeof message.result === 'object' && - 'stopReason' in message.result && - message.result.stopReason === 'end_turn' - ) { - callbacks.onEndTurn(); + + if (message.result && typeof message.result === 'object') { + const stopReasonValue = + (message.result as { stopReason?: unknown }).stopReason ?? + (message.result as { stop_reason?: unknown }).stop_reason; + if (typeof stopReasonValue === 'string') { + callbacks.onEndTurn(stopReasonValue); + } else if ( + 'stopReason' in message.result || + 'stop_reason' in message.result + ) { + // stop_reason present but not a string (e.g., null) -> still emit + callbacks.onEndTurn(); + } } resolve(message.result); } else if ('error' in message) { @@ -161,6 +169,15 @@ export class AcpMessageHandler { ); callbacks.onSessionUpdate(params as AcpSessionUpdate); break; + case CLIENT_METHODS.authenticate_update: + console.log( + '[ACP] >>> Processing authenticate_update:', + JSON.stringify(params).substring(0, 300), + ); + callbacks.onAuthenticateUpdate( + params as AuthenticateUpdateNotification, + ); + break; case CLIENT_METHODS.session_request_permission: result = await this.handlePermissionRequest( params as AcpPermissionRequest, diff --git a/packages/vscode-ide-companion/src/services/acpSessionManager.ts b/packages/vscode-ide-companion/src/services/acpSessionManager.ts index 8812282a..cfa299bf 100644 --- a/packages/vscode-ide-companion/src/services/acpSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/acpSessionManager.ts @@ -14,8 +14,8 @@ import type { AcpRequest, AcpNotification, AcpResponse, - ApprovalModeValue, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { AGENT_METHODS } from '../constants/acpSchema.js'; import type { PendingRequest } from '../types/connectionTypes.js'; import type { ChildProcess } from 'child_process'; @@ -54,8 +54,14 @@ export class AcpSessionManager { }; return new Promise((resolve, reject) => { - const timeoutDuration = - method === AGENT_METHODS.session_prompt ? 120000 : 60000; + // different timeout durations based on methods + let timeoutDuration = 60000; // default 60 seconds + if ( + method === AGENT_METHODS.session_prompt || + method === AGENT_METHODS.initialize + ) { + timeoutDuration = 120000; // 2min for session_prompt and initialize + } const timeoutId = setTimeout(() => { pendingRequests.delete(id); @@ -163,7 +169,7 @@ export class AcpSessionManager { pendingRequests, nextRequestId, ); - console.log('[ACP] Authenticate successful'); + console.log('[ACP] Authenticate successful', response); return response; } diff --git a/packages/vscode-ide-companion/src/services/authStateManager.ts b/packages/vscode-ide-companion/src/services/authStateManager.ts deleted file mode 100644 index 566a4afb..00000000 --- a/packages/vscode-ide-companion/src/services/authStateManager.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import type * as vscode from 'vscode'; - -interface AuthState { - isAuthenticated: boolean; - authMethod: string; - timestamp: number; - workingDir?: string; -} - -/** - * Manages authentication state caching to avoid repeated logins - */ -export class AuthStateManager { - private static instance: AuthStateManager | null = null; - private static context: vscode.ExtensionContext | null = null; - private static readonly AUTH_STATE_KEY = 'qwen.authState'; - private static readonly AUTH_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours - private constructor() {} - - /** - * Get singleton instance of AuthStateManager - */ - static getInstance(context?: vscode.ExtensionContext): AuthStateManager { - if (!AuthStateManager.instance) { - AuthStateManager.instance = new AuthStateManager(); - } - - // If a context is provided, update the static context - if (context) { - AuthStateManager.context = context; - } - - return AuthStateManager.instance; - } - - /** - * Check if there's a valid cached authentication - */ - async hasValidAuth(workingDir: string, authMethod: string): Promise { - const state = await this.getAuthState(); - - if (!state) { - console.log('[AuthStateManager] No cached auth state found'); - return false; - } - - console.log('[AuthStateManager] Found cached auth state:', { - workingDir: state.workingDir, - authMethod: state.authMethod, - timestamp: new Date(state.timestamp).toISOString(), - isAuthenticated: state.isAuthenticated, - }); - console.log('[AuthStateManager] Checking against:', { - workingDir, - authMethod, - }); - - // Check if auth is still valid (within cache duration) - const now = Date.now(); - const isExpired = - now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; - - if (isExpired) { - console.log('[AuthStateManager] Cached auth expired'); - console.log( - '[AuthStateManager] Cache age:', - Math.floor((now - state.timestamp) / 1000 / 60), - 'minutes', - ); - await this.clearAuthState(); - return false; - } - - // Check if it's for the same working directory and auth method - const isSameContext = - state.workingDir === workingDir && state.authMethod === authMethod; - - if (!isSameContext) { - console.log('[AuthStateManager] Working dir or auth method changed'); - console.log('[AuthStateManager] Cached workingDir:', state.workingDir); - console.log('[AuthStateManager] Current workingDir:', workingDir); - console.log('[AuthStateManager] Cached authMethod:', state.authMethod); - console.log('[AuthStateManager] Current authMethod:', authMethod); - return false; - } - - console.log('[AuthStateManager] Valid cached auth found'); - return state.isAuthenticated; - } - - /** - * Force check auth state without clearing cache - * This is useful for debugging to see what's actually cached - */ - async debugAuthState(): Promise { - const state = await this.getAuthState(); - console.log('[AuthStateManager] DEBUG - Current auth state:', state); - - if (state) { - const now = Date.now(); - const age = Math.floor((now - state.timestamp) / 1000 / 60); - const isExpired = - now - state.timestamp > AuthStateManager.AUTH_CACHE_DURATION; - - console.log('[AuthStateManager] DEBUG - Auth state age:', age, 'minutes'); - console.log('[AuthStateManager] DEBUG - Auth state expired:', isExpired); - console.log( - '[AuthStateManager] DEBUG - Auth state valid:', - state.isAuthenticated, - ); - } - } - - /** - * Save successful authentication state - */ - async saveAuthState(workingDir: string, authMethod: string): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - throw new Error( - '[AuthStateManager] No context available for saving auth state', - ); - } - - const state: AuthState = { - isAuthenticated: true, - authMethod, - workingDir, - timestamp: Date.now(), - }; - - console.log('[AuthStateManager] Saving auth state:', { - workingDir, - authMethod, - timestamp: new Date(state.timestamp).toISOString(), - }); - - await AuthStateManager.context.globalState.update( - AuthStateManager.AUTH_STATE_KEY, - state, - ); - console.log('[AuthStateManager] Auth state saved'); - - // Verify the state was saved correctly - const savedState = await this.getAuthState(); - console.log('[AuthStateManager] Verified saved state:', savedState); - } - - /** - * Clear authentication state - */ - async clearAuthState(): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - throw new Error( - '[AuthStateManager] No context available for clearing auth state', - ); - } - - console.log('[AuthStateManager] Clearing auth state'); - const currentState = await this.getAuthState(); - console.log( - '[AuthStateManager] Current state before clearing:', - currentState, - ); - - await AuthStateManager.context.globalState.update( - AuthStateManager.AUTH_STATE_KEY, - undefined, - ); - console.log('[AuthStateManager] Auth state cleared'); - - // Verify the state was cleared - const newState = await this.getAuthState(); - console.log('[AuthStateManager] State after clearing:', newState); - } - - /** - * Get current auth state - */ - private async getAuthState(): Promise { - // Ensure we have a valid context - if (!AuthStateManager.context) { - console.log( - '[AuthStateManager] No context available for getting auth state', - ); - return undefined; - } - - const a = AuthStateManager.context.globalState.get( - AuthStateManager.AUTH_STATE_KEY, - ); - console.log('[AuthStateManager] Auth state:', a); - return a; - } - - /** - * Get auth state info for debugging - */ - async getAuthInfo(): Promise { - const state = await this.getAuthState(); - if (!state) { - return 'No cached auth'; - } - - const age = Math.floor((Date.now() - state.timestamp) / 1000 / 60); - return `Auth cached ${age}m ago, method: ${state.authMethod}`; - } -} diff --git a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts index a57d15b7..e60ee3a2 100644 --- a/packages/vscode-ide-companion/src/services/qwenAgentManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenAgentManager.ts @@ -7,22 +7,25 @@ import { AcpConnection } from './acpConnection.js'; import type { AcpSessionUpdate, AcpPermissionRequest, - ApprovalModeValue, + AuthenticateUpdateNotification, } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import { QwenSessionReader, type QwenSession } from './qwenSessionReader.js'; import { QwenSessionManager } from './qwenSessionManager.js'; -import type { AuthStateManager } from './authStateManager.js'; import type { ChatMessage, PlanEntry, ToolCallUpdateData, QwenAgentCallbacks, } from '../types/chatTypes.js'; -import { QwenConnectionHandler } from '../services/qwenConnectionHandler.js'; +import { + QwenConnectionHandler, + type QwenConnectionResult, +} from '../services/qwenConnectionHandler.js'; import { QwenSessionUpdateHandler } from './qwenSessionUpdateHandler.js'; -import { CliContextManager } from '../cli/cliContextManager.js'; import { authMethod } from '../types/acpTypes.js'; -import { MIN_CLI_VERSION_FOR_SESSION_METHODS } from '../cli/cliVersionManager.js'; +import { isAuthenticationRequiredError } from '../utils/authErrors.js'; +import { handleAuthenticateUpdate } from '../utils/authNotificationHandler.js'; export type { ChatMessage, PlanEntry, ToolCallUpdateData }; @@ -31,6 +34,13 @@ export type { ChatMessage, PlanEntry, ToolCallUpdateData }; * * Coordinates various modules and provides unified interface */ +interface AgentConnectOptions { + autoAuthenticate?: boolean; +} +interface AgentSessionOptions { + autoAuthenticate?: boolean; +} + export class QwenAgentManager { private connection: AcpConnection; private sessionReader: QwenSessionReader; @@ -42,9 +52,9 @@ export class QwenAgentManager { // session/update notifications. We set this flag to route message chunks // (user/assistant) as discrete chat messages instead of live streaming. private rehydratingSessionId: string | null = null; - // Cache the last used AuthStateManager so internal calls (e.g. fallback paths) - // can reuse it and avoid forcing a fresh authentication unnecessarily. - private defaultAuthStateManager?: AuthStateManager; + // CLI is now the single source of truth for authentication state + // Deduplicate concurrent session/new attempts + private sessionCreateInFlight: Promise | null = null; // Callback storage private callbacks: QwenAgentCallbacks = {}; @@ -120,10 +130,10 @@ export class QwenAgentManager { return { optionId: 'allow_once' }; }; - this.connection.onEndTurn = () => { + this.connection.onEndTurn = (reason?: string) => { try { if (this.callbacks.onEndTurn) { - this.callbacks.onEndTurn(); + this.callbacks.onEndTurn(reason); } else if (this.callbacks.onStreamChunk) { // Fallback: send a zero-length chunk then rely on streamEnd elsewhere this.callbacks.onStreamChunk(''); @@ -133,6 +143,20 @@ export class QwenAgentManager { } }; + this.connection.onAuthenticateUpdate = ( + data: AuthenticateUpdateNotification, + ) => { + try { + // Handle authentication update notifications by showing VS Code notification + handleAuthenticateUpdate(data); + } catch (err) { + console.warn( + '[QwenAgentManager] onAuthenticateUpdate callback error:', + err, + ); + } + }; + // Initialize callback to surface available modes and current mode to UI this.connection.onInitialized = (init: unknown) => { try { @@ -163,23 +187,19 @@ export class QwenAgentManager { * Connect to Qwen service * * @param workingDir - Working directory - * @param authStateManager - Authentication state manager (optional) - * @param cliPath - CLI path (optional, if provided will override the path in configuration) + * @param cliEntryPath - Path to bundled CLI entrypoint (cli.js) */ async connect( workingDir: string, - authStateManager?: AuthStateManager, - _cliPath?: string, - ): Promise { + cliEntryPath: string, + options?: AgentConnectOptions, + ): Promise { this.currentWorkingDir = workingDir; - // Remember the provided authStateManager for future calls - this.defaultAuthStateManager = authStateManager; - await this.connectionHandler.connect( + return this.connectionHandler.connect( this.connection, - this.sessionReader, workingDir, - authStateManager, - _cliPath, + cliEntryPath, + options, ); } @@ -261,71 +281,59 @@ export class QwenAgentManager { '[QwenAgentManager] Getting session list with version-aware strategy', ); - // Check if CLI supports session/list method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.supportsSessionList(); + try { + console.log( + '[QwenAgentManager] Attempting to get session list via ACP method', + ); + const response = await this.connection.listSessions(); + console.log('[QwenAgentManager] ACP session list response:', response); - console.log( - '[QwenAgentManager] CLI supports session/list:', - supportsSessionList, - ); + // sendRequest resolves with the JSON-RPC "result" directly + // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } + // Older prototypes might return an array. Support both. + const res: unknown = response; + let items: Array> = []; - // Try ACP method first if supported - if (supportsSessionList) { - try { - console.log( - '[QwenAgentManager] Attempting to get session list via ACP method', - ); - const response = await this.connection.listSessions(); - console.log('[QwenAgentManager] ACP session list response:', response); + // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC + // "result" directly (not the full AcpResponse). Treat it as unknown + // and carefully narrow before accessing `items` to satisfy strict TS. + if (res && typeof res === 'object' && 'items' in res) { + const itemsValue = (res as { items?: unknown }).items; + items = Array.isArray(itemsValue) + ? (itemsValue as Array>) + : []; + } - // sendRequest resolves with the JSON-RPC "result" directly - // Newer CLI returns an object: { items: [...], nextCursor?, hasMore } - // Older prototypes might return an array. Support both. - const res: unknown = response; - let items: Array> = []; - - // Note: AcpSessionManager resolves `sendRequest` with the JSON-RPC - // "result" directly (not the full AcpResponse). Treat it as unknown - // and carefully narrow before accessing `items` to satisfy strict TS. - if (res && typeof res === 'object' && 'items' in res) { - const itemsValue = (res as { items?: unknown }).items; - items = Array.isArray(itemsValue) - ? (itemsValue as Array>) - : []; - } + console.log( + '[QwenAgentManager] Sessions retrieved via ACP:', + res, + items.length, + ); + if (items.length > 0) { + const sessions = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); console.log( '[QwenAgentManager] Sessions retrieved via ACP:', - res, - items.length, - ); - if (items.length > 0) { - const sessions = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); - - console.log( - '[QwenAgentManager] Sessions retrieved via ACP:', - sessions.length, - ); - return sessions; - } - } catch (error) { - console.warn( - '[QwenAgentManager] ACP session list failed, falling back to file system method:', - error, + sessions.length, ); + return sessions; } + } catch (error) { + console.warn( + '[QwenAgentManager] ACP session list failed, falling back to file system method:', + error, + ); } // Always fall back to file system method @@ -345,8 +353,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(session), startTime: session.startTime, lastUpdated: session.lastUpdated, - messageCount: session.messages.length, + messageCount: session.messageCount ?? session.messages.length, projectHash: session.projectHash, + filePath: session.filePath, + cwd: session.cwd, }), ); @@ -380,62 +390,52 @@ export class QwenAgentManager { const size = params?.size ?? 20; const cursor = params?.cursor; - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionList = cliContextManager.supportsSessionList(); + try { + const response = await this.connection.listSessions({ + size, + ...(cursor !== undefined ? { cursor } : {}), + }); + // sendRequest resolves with the JSON-RPC "result" directly + const res: unknown = response; + let items: Array> = []; - if (supportsSessionList) { - try { - const response = await this.connection.listSessions({ - size, - ...(cursor !== undefined ? { cursor } : {}), - }); - // sendRequest resolves with the JSON-RPC "result" directly - const res: unknown = response; - let items: Array> = []; - - if (Array.isArray(res)) { - items = res; - } else if (typeof res === 'object' && res !== null && 'items' in res) { - const responseObject = res as { - items?: Array>; - }; - items = Array.isArray(responseObject.items) - ? responseObject.items - : []; - } - - const mapped = items.map((item) => ({ - id: item.sessionId || item.id, - sessionId: item.sessionId || item.id, - title: item.title || item.name || item.prompt || 'Untitled Session', - name: item.title || item.name || item.prompt || 'Untitled Session', - startTime: item.startTime, - lastUpdated: item.mtime || item.lastUpdated, - messageCount: item.messageCount || 0, - projectHash: item.projectHash, - filePath: item.filePath, - cwd: item.cwd, - })); - - const nextCursor: number | undefined = - typeof res === 'object' && res !== null && 'nextCursor' in res - ? typeof res.nextCursor === 'number' - ? res.nextCursor - : undefined - : undefined; - const hasMore: boolean = - typeof res === 'object' && res !== null && 'hasMore' in res - ? Boolean(res.hasMore) - : false; - - return { sessions: mapped, nextCursor, hasMore }; - } catch (error) { - console.warn( - '[QwenAgentManager] Paged ACP session list failed:', - error, - ); - // fall through to file system + if (Array.isArray(res)) { + items = res; + } else if (typeof res === 'object' && res !== null && 'items' in res) { + const responseObject = res as { + items?: Array>; + }; + items = Array.isArray(responseObject.items) ? responseObject.items : []; } + + const mapped = items.map((item) => ({ + id: item.sessionId || item.id, + sessionId: item.sessionId || item.id, + title: item.title || item.name || item.prompt || 'Untitled Session', + name: item.title || item.name || item.prompt || 'Untitled Session', + startTime: item.startTime, + lastUpdated: item.mtime || item.lastUpdated, + messageCount: item.messageCount || 0, + projectHash: item.projectHash, + filePath: item.filePath, + cwd: item.cwd, + })); + + const nextCursor: number | undefined = + typeof res === 'object' && res !== null && 'nextCursor' in res + ? typeof res.nextCursor === 'number' + ? res.nextCursor + : undefined + : undefined; + const hasMore: boolean = + typeof res === 'object' && res !== null && 'hasMore' in res + ? Boolean(res.hasMore) + : false; + + return { sessions: mapped, nextCursor, hasMore }; + } catch (error) { + console.warn('[QwenAgentManager] Paged ACP session list failed:', error); + // fall through to file system } // Fallback: file system for current project only (to match ACP semantics) @@ -461,8 +461,10 @@ export class QwenAgentManager { name: this.sessionReader.getSessionTitle(x.raw), startTime: x.raw.startTime, lastUpdated: x.raw.lastUpdated, - messageCount: x.raw.messages.length, + messageCount: x.raw.messageCount ?? x.raw.messages.length, projectHash: x.raw.projectHash, + filePath: x.raw.filePath, + cwd: x.raw.cwd, })); const nextCursorVal = page.length > 0 ? page[page.length - 1].mtime : undefined; @@ -482,32 +484,28 @@ export class QwenAgentManager { */ async getSessionMessages(sessionId: string): Promise { try { - // Prefer reading CLI's JSONL if we can find filePath from session/list - const cliContextManager = CliContextManager.getInstance(); - if (cliContextManager.supportsSessionList()) { - try { - const list = await this.getSessionList(); - const item = list.find( - (s) => s.sessionId === sessionId || s.id === sessionId, - ); - console.log( - '[QwenAgentManager] Session list item for filePath lookup:', - item, - ); - if ( - typeof item === 'object' && - item !== null && - 'filePath' in item && - typeof item.filePath === 'string' - ) { - const messages = await this.readJsonlMessages(item.filePath); - // Even if messages array is empty, we should return it rather than falling back - // This ensures we don't accidentally show messages from a different session format - return messages; - } - } catch (e) { - console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); + try { + const list = await this.getSessionList(); + const item = list.find( + (s) => s.sessionId === sessionId || s.id === sessionId, + ); + console.log( + '[QwenAgentManager] Session list item for filePath lookup:', + item, + ); + if ( + typeof item === 'object' && + item !== null && + 'filePath' in item && + typeof item.filePath === 'string' + ) { + const messages = await this.readJsonlMessages(item.filePath); + // Even if messages array is empty, we should return it rather than falling back + // This ensures we don't accidentally show messages from a different session format + return messages; } + } catch (e) { + console.warn('[QwenAgentManager] JSONL read path lookup failed:', e); } // Fallback: legacy JSON session files @@ -705,7 +703,9 @@ export class QwenAgentManager { const planText = planEntries .map( (entry: Record, index: number) => - `${index + 1}. ${entry.description || entry.title || 'Unnamed step'}`, + `${index + 1}. ${ + entry.description || entry.title || 'Unnamed step' + }`, ) .join('\n'); msgs.push({ @@ -900,80 +900,6 @@ export class QwenAgentManager { return this.saveSessionViaCommand(sessionId, tag); } - /** - * Save session as checkpoint (using CLI format) - * Saves to ~/.qwen/tmp/{projectHash}/checkpoint-{tag}.json - * Saves two copies with sessionId and conversationId to ensure recovery via either ID - * - * @param messages - Current session messages - * @param conversationId - Conversation ID (from VSCode extension) - * @returns Save result - */ - async saveCheckpoint( - messages: ChatMessage[], - conversationId: string, - ): Promise<{ success: boolean; tag?: string; message?: string }> { - try { - console.log('[QwenAgentManager] ===== CHECKPOINT SAVE START ====='); - console.log('[QwenAgentManager] Conversation ID:', conversationId); - console.log('[QwenAgentManager] Message count:', messages.length); - console.log( - '[QwenAgentManager] Current working dir:', - this.currentWorkingDir, - ); - console.log( - '[QwenAgentManager] Current session ID (from CLI):', - this.currentSessionId, - ); - // In ACP mode, the CLI does not accept arbitrary slash commands like - // "/chat save". To ensure we never block on unsupported features, - // persist checkpoints directly to ~/.qwen/tmp using our SessionManager. - const qwenMessages = messages.map((m) => ({ - // Generate minimal QwenMessage shape expected by the writer - id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - type: m.role === 'user' ? ('user' as const) : ('qwen' as const), - content: m.content, - })); - - const tag = await this.sessionManager.saveCheckpoint( - qwenMessages, - conversationId, - this.currentWorkingDir, - this.currentSessionId || undefined, - ); - - return { success: true, tag }; - } catch (error) { - console.error('[QwenAgentManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenAgentManager] Error:', error); - console.error( - '[QwenAgentManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - return { - success: false, - message: error instanceof Error ? error.message : String(error), - }; - } - } - - /** - * Save session directly to file system (without relying on ACP) - * - * @param messages - Current session messages - * @param sessionName - Session name - * @returns Save result - */ - async saveSessionDirect( - messages: ChatMessage[], - sessionName: string, - ): Promise<{ success: boolean; sessionId?: string; message?: string }> { - // Use checkpoint format instead of session format - // This matches CLI's /chat save behavior - return this.saveCheckpoint(messages, sessionName); - } - /** * Try to load session via ACP session/load method * This method will only be used if CLI version supports it @@ -985,16 +911,6 @@ export class QwenAgentManager { sessionId: string, cwdOverride?: string, ): Promise { - // Check if CLI supports session/load method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionLoad = cliContextManager.supportsSessionLoad(); - - if (!supportsSessionLoad) { - throw new Error( - `CLI version does not support session/load method. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, - ); - } - try { // Route upcoming session/update messages as discrete messages for replay this.rehydratingSessionId = sessionId; @@ -1068,32 +984,20 @@ export class QwenAgentManager { sessionId, ); - // Check if CLI supports session/load method - const cliContextManager = CliContextManager.getInstance(); - const supportsSessionLoad = cliContextManager.supportsSessionLoad(); + try { + console.log( + '[QwenAgentManager] Attempting to load session via ACP method', + ); + await this.loadSessionViaAcp(sessionId); + console.log('[QwenAgentManager] Session loaded successfully via ACP'); - console.log( - '[QwenAgentManager] CLI supports session/load:', - supportsSessionLoad, - ); - - // Try ACP method first if supported - if (supportsSessionLoad) { - try { - console.log( - '[QwenAgentManager] Attempting to load session via ACP method', - ); - await this.loadSessionViaAcp(sessionId); - console.log('[QwenAgentManager] Session loaded successfully via ACP'); - - // After loading via ACP, we still need to get messages from file system - // In future, we might get them directly from the ACP response - } catch (error) { - console.warn( - '[QwenAgentManager] ACP session load failed, falling back to file system method:', - error, - ); - } + // After loading via ACP, we still need to get messages from file system + // In future, we might get them directly from the ACP response + } catch (error) { + console.warn( + '[QwenAgentManager] ACP session load failed, falling back to file system method:', + error, + ); } // Always fall back to file system method @@ -1161,16 +1065,6 @@ export class QwenAgentManager { } } - /** - * Load session, preferring ACP method if CLI version supports it - * - * @param sessionId - Session ID - * @returns Loaded session messages or null - */ - async loadSessionDirect(sessionId: string): Promise { - return this.loadSession(sessionId); - } - /** * Create new session * @@ -1181,95 +1075,70 @@ export class QwenAgentManager { */ async createNewSession( workingDir: string, - authStateManager?: AuthStateManager, + options?: AgentSessionOptions, ): Promise { + const autoAuthenticate = options?.autoAuthenticate ?? true; + // Reuse existing session if present + if (this.connection.currentSessionId) { + return this.connection.currentSessionId; + } + // Deduplicate concurrent session/new attempts + if (this.sessionCreateInFlight) { + return this.sessionCreateInFlight; + } + console.log('[QwenAgentManager] Creating new session...'); - // Check if we have valid cached authentication - let hasValidAuth = false; - // Prefer the provided authStateManager, otherwise fall back to the one - // remembered during connect(). This prevents accidental re-auth in - // fallback paths (e.g. session switching) when the handler didn't pass it. - const effectiveAuth = authStateManager || this.defaultAuthStateManager; - if (effectiveAuth) { - hasValidAuth = await effectiveAuth.hasValidAuth(workingDir, authMethod); - console.log( - '[QwenAgentManager] Has valid cached auth for new session:', - hasValidAuth, - ); - } - - // Only authenticate if we don't have valid cached auth - if (!hasValidAuth) { - console.log( - '[QwenAgentManager] Authenticating before creating session...', - ); + this.sessionCreateInFlight = (async () => { try { - await this.connection.authenticate(authMethod); - console.log('[QwenAgentManager] Authentication successful'); - - // Save auth state - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Saving auth state after successful authentication', - ); - await effectiveAuth.saveAuthState(workingDir, authMethod); - } - } catch (authError) { - console.error('[QwenAgentManager] Authentication failed:', authError); - // Clear potentially invalid cache - if (effectiveAuth) { - console.log( - '[QwenAgentManager] Clearing auth cache due to authentication failure', - ); - await effectiveAuth.clearAuthState(); - } - throw authError; - } - } else { - console.log( - '[QwenAgentManager] Skipping authentication - using valid cached auth', - ); - } - - // Try to create a new ACP session. If Qwen asks for auth despite our - // cached flag (e.g. fresh process or expired tokens), re-authenticate and retry. - try { - await this.connection.newSession(workingDir); - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - const requiresAuth = - msg.includes('Authentication required') || - msg.includes('(code: -32000)'); - - if (requiresAuth) { - console.warn( - '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', - ); + // Try to create a new ACP session. If Qwen asks for auth, let it handle authentication. try { - await this.connection.authenticate(authMethod); - // Persist auth cache so subsequent calls can skip the web flow. - if (effectiveAuth) { - await effectiveAuth.saveAuthState(workingDir, authMethod); - } await this.connection.newSession(workingDir); - } catch (reauthErr) { - // Clear potentially stale cache on failure and rethrow - if (effectiveAuth) { - await effectiveAuth.clearAuthState(); + } catch (err) { + const requiresAuth = isAuthenticationRequiredError(err); + + if (requiresAuth) { + if (!autoAuthenticate) { + console.warn( + '[QwenAgentManager] session/new requires authentication but auto-auth is disabled. Deferring until user logs in.', + ); + throw err; + } + console.warn( + '[QwenAgentManager] session/new requires authentication. Retrying with authenticate...', + ); + try { + // Let CLI handle authentication - it's the single source of truth + await this.connection.authenticate(authMethod); + console.log( + '[QwenAgentManager] createNewSession Authentication successful. Retrying session/new...', + ); + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); + await this.connection.newSession(workingDir); + } catch (reauthErr) { + console.error( + '[QwenAgentManager] Re-authentication failed:', + reauthErr, + ); + throw reauthErr; + } + } else { + throw err; } - throw reauthErr; } - } else { - throw err; + const newSessionId = this.connection.currentSessionId; + console.log( + '[QwenAgentManager] New session created with ID:', + newSessionId, + ); + return newSessionId; + } finally { + this.sessionCreateInFlight = null; } - } - const newSessionId = this.connection.currentSessionId; - console.log( - '[QwenAgentManager] New session created with ID:', - newSessionId, - ); - return newSessionId; + })(); + + return this.sessionCreateInFlight; } /** @@ -1354,9 +1223,9 @@ export class QwenAgentManager { /** * Register end-of-turn callback * - * @param callback - Called when ACP stopReason === 'end_turn' + * @param callback - Called when ACP stopReason is reported */ - onEndTurn(callback: () => void): void { + onEndTurn(callback: (reason?: string) => void): void { this.callbacks.onEndTurn = callback; this.sessionUpdateHandler.updateCallbacks(this.callbacks); } diff --git a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts index 11e7199a..c66ee23c 100644 --- a/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenConnectionHandler.ts @@ -10,17 +10,15 @@ * Handles Qwen Agent connection establishment, authentication, and session creation */ -import * as vscode from 'vscode'; import type { AcpConnection } from './acpConnection.js'; -import type { QwenSessionReader } from '../services/qwenSessionReader.js'; -import type { AuthStateManager } from '../services/authStateManager.js'; -import { - CliVersionManager, - MIN_CLI_VERSION_FOR_SESSION_METHODS, -} from '../cli/cliVersionManager.js'; -import { CliContextManager } from '../cli/cliContextManager.js'; +import { isAuthenticationRequiredError } from '../utils/authErrors.js'; import { authMethod } from '../types/acpTypes.js'; +export interface QwenConnectionResult { + sessionCreated: boolean; + requiresAuth: boolean; +} + /** * Qwen Connection Handler class * Handles connection, authentication, and session initialization @@ -30,62 +28,27 @@ export class QwenConnectionHandler { * Connect to Qwen service and establish session * * @param connection - ACP connection instance - * @param sessionReader - Session reader instance * @param workingDir - Working directory - * @param authStateManager - Authentication state manager (optional) * @param cliPath - CLI path (optional, if provided will override the path in configuration) */ async connect( connection: AcpConnection, - sessionReader: QwenSessionReader, workingDir: string, - authStateManager?: AuthStateManager, - cliPath?: string, - ): Promise { + cliEntryPath: string, + options?: { + autoAuthenticate?: boolean; + }, + ): Promise { const connectId = Date.now(); console.log(`[QwenAgentManager] 🚀 CONNECT() CALLED - ID: ${connectId}`); - - // Check CLI version and features - const cliVersionManager = CliVersionManager.getInstance(); - const versionInfo = await cliVersionManager.detectCliVersion(); - console.log('[QwenAgentManager] CLI version info:', versionInfo); - - // Store CLI context - const cliContextManager = CliContextManager.getInstance(); - cliContextManager.setCurrentVersionInfo(versionInfo); - - // Show warning if CLI version is below minimum requirement - if (!versionInfo.isSupported) { - // Wait to determine release version number - vscode.window.showWarningMessage( - `Qwen Code CLI version ${versionInfo.version} is below the minimum required version. Some features may not work properly. Please upgrade to version ${MIN_CLI_VERSION_FOR_SESSION_METHODS} or later.`, - ); - } - - const config = vscode.workspace.getConfiguration('qwenCode'); - // Use the provided CLI path if available, otherwise use the configured path - const effectiveCliPath = - cliPath || config.get('qwen.cliPath', 'qwen'); + const autoAuthenticate = options?.autoAuthenticate ?? true; + let sessionCreated = false; + let requiresAuth = false; // Build extra CLI arguments (only essential parameters) const extraArgs: string[] = []; - await connection.connect(effectiveCliPath, workingDir, extraArgs); - - // Check if we have valid cached authentication - if (authStateManager) { - console.log('[QwenAgentManager] Checking for cached authentication...'); - console.log('[QwenAgentManager] Working dir:', workingDir); - console.log('[QwenAgentManager] Auth method:', authMethod); - - const hasValidAuth = await authStateManager.hasValidAuth( - workingDir, - authMethod, - ); - console.log('[QwenAgentManager] Has valid auth:', hasValidAuth); - } else { - console.log('[QwenAgentManager] No authStateManager provided'); - } + await connection.connect(cliEntryPath!, workingDir, extraArgs); // Try to restore existing session or create new session // Note: Auto-restore on connect is disabled to avoid surprising loads @@ -99,88 +62,44 @@ export class QwenConnectionHandler { '[QwenAgentManager] no sessionRestored, Creating new session...', ); - // Check if we have valid cached authentication - let hasValidAuth = false; - if (authStateManager) { - hasValidAuth = await authStateManager.hasValidAuth( - workingDir, - authMethod, - ); - } - - // Only authenticate if we don't have valid cached auth - if (!hasValidAuth) { - console.log( - '[QwenAgentManager] Authenticating before creating session...', - ); - try { - await connection.authenticate(authMethod); - console.log('[QwenAgentManager] Authentication successful'); - - // Save auth state - if (authStateManager) { - console.log( - '[QwenAgentManager] Saving auth state after successful authentication', - ); - console.log('[QwenAgentManager] Working dir for save:', workingDir); - console.log('[QwenAgentManager] Auth method for save:', authMethod); - await authStateManager.saveAuthState(workingDir, authMethod); - console.log('[QwenAgentManager] Auth state save completed'); - } - } catch (authError) { - console.error('[QwenAgentManager] Authentication failed:', authError); - // Clear potentially invalid cache - if (authStateManager) { - console.log( - '[QwenAgentManager] Clearing auth cache due to authentication failure', - ); - await authStateManager.clearAuthState(); - } - throw authError; - } - } else { - console.log( - '[QwenAgentManager] Skipping authentication - using valid cached auth', - ); - } - try { console.log( - '[QwenAgentManager] Creating new session after authentication...', + '[QwenAgentManager] Creating new session (letting CLI handle authentication)...', ); await this.newSessionWithRetry( connection, workingDir, 3, authMethod, - authStateManager, + autoAuthenticate, ); console.log('[QwenAgentManager] New session created successfully'); - - // Ensure auth state is saved (prevent repeated authentication) - if (authStateManager) { - console.log( - '[QwenAgentManager] Saving auth state after successful session creation', - ); - await authStateManager.saveAuthState(workingDir, authMethod); - } + sessionCreated = true; } catch (sessionError) { - console.log(`\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`); - console.log(`[QwenAgentManager] Error details:`, sessionError); - - // Clear cache - if (authStateManager) { - console.log('[QwenAgentManager] Clearing auth cache due to failure'); - await authStateManager.clearAuthState(); + const needsAuth = + autoAuthenticate === false && + isAuthenticationRequiredError(sessionError); + if (needsAuth) { + requiresAuth = true; + console.log( + '[QwenAgentManager] Session creation requires authentication; waiting for user-triggered login.', + ); + } else { + console.log( + `\n⚠️ [SESSION FAILED] newSessionWithRetry threw error\n`, + ); + console.log(`[QwenAgentManager] Error details:`, sessionError); + throw sessionError; } - - throw sessionError; } + } else { + sessionCreated = true; } console.log(`\n========================================`); console.log(`[QwenAgentManager] ✅ CONNECT() COMPLETED SUCCESSFULLY`); console.log(`========================================\n`); + return { sessionCreated, requiresAuth }; } /** @@ -195,7 +114,7 @@ export class QwenConnectionHandler { workingDir: string, maxRetries: number, authMethod: string, - authStateManager?: AuthStateManager, + autoAuthenticate: boolean, ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { @@ -215,18 +134,26 @@ export class QwenConnectionHandler { // If Qwen reports that authentication is required, try to // authenticate on-the-fly once and retry without waiting. - const requiresAuth = - errorMessage.includes('Authentication required') || - errorMessage.includes('(code: -32000)'); + const requiresAuth = isAuthenticationRequiredError(error); if (requiresAuth) { + if (!autoAuthenticate) { + console.log( + '[QwenAgentManager] Authentication required but auto-authentication is disabled. Propagating error.', + ); + throw error; + } console.log( '[QwenAgentManager] Qwen requires authentication. Authenticating and retrying session/new...', ); try { await connection.authenticate(authMethod); - if (authStateManager) { - await authStateManager.saveAuthState(workingDir, authMethod); - } + // FIXME: @yiliang114 If there is no delay for a while, immediately executing + // newSession may cause the cli authorization jump to be triggered again + // Add a slight delay to ensure auth state is settled + await new Promise((resolve) => setTimeout(resolve, 300)); + console.log( + '[QwenAgentManager] newSessionWithRetry Authentication successful', + ); // Retry immediately after successful auth await connection.newSession(workingDir); console.log( @@ -238,9 +165,6 @@ export class QwenConnectionHandler { '[QwenAgentManager] Re-authentication failed:', authErr, ); - if (authStateManager) { - await authStateManager.clearAuthState(); - } // Fall through to retry logic below } } diff --git a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts index 2bd609bb..9336a060 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionManager.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionManager.ts @@ -51,131 +51,7 @@ export class QwenSessionManager { } /** - * Save current conversation as a checkpoint (matching CLI's /chat save format) - * Creates checkpoint with BOTH conversationId and sessionId as tags for compatibility - * - * @param messages - Current conversation messages - * @param conversationId - Conversation ID (from VSCode extension) - * @param sessionId - Session ID (from CLI tmp session file, optional) - * @param workingDir - Current working directory - * @returns Checkpoint tag - */ - async saveCheckpoint( - messages: QwenMessage[], - conversationId: string, - workingDir: string, - sessionId?: string, - ): Promise { - try { - console.log('[QwenSessionManager] ===== SAVEPOINT START ====='); - console.log('[QwenSessionManager] Conversation ID:', conversationId); - console.log( - '[QwenSessionManager] Session ID:', - sessionId || 'not provided', - ); - console.log('[QwenSessionManager] Working dir:', workingDir); - console.log('[QwenSessionManager] Message count:', messages.length); - - // Get project directory (parent of chats directory) - const projectHash = this.getProjectHash(workingDir); - console.log('[QwenSessionManager] Project hash:', projectHash); - - const projectDir = path.join(this.qwenDir, 'tmp', projectHash); - console.log('[QwenSessionManager] Project dir:', projectDir); - - if (!fs.existsSync(projectDir)) { - console.log('[QwenSessionManager] Creating project directory...'); - fs.mkdirSync(projectDir, { recursive: true }); - console.log('[QwenSessionManager] Directory created'); - } else { - console.log('[QwenSessionManager] Project directory already exists'); - } - - // Convert messages to checkpoint format (Gemini-style messages) - console.log( - '[QwenSessionManager] Converting messages to checkpoint format...', - ); - const checkpointMessages = messages.map((msg, index) => { - console.log( - `[QwenSessionManager] Message ${index}: type=${msg.type}, contentLength=${msg.content?.length || 0}`, - ); - return { - role: msg.type === 'user' ? 'user' : 'model', - parts: [ - { - text: msg.content, - }, - ], - }; - }); - - console.log( - '[QwenSessionManager] Converted', - checkpointMessages.length, - 'messages', - ); - - const jsonContent = JSON.stringify(checkpointMessages, null, 2); - console.log( - '[QwenSessionManager] JSON content length:', - jsonContent.length, - ); - - // Save with conversationId as primary tag - const convFilename = `checkpoint-${conversationId}.json`; - const convFilePath = path.join(projectDir, convFilename); - console.log( - '[QwenSessionManager] Saving checkpoint with conversationId:', - convFilePath, - ); - fs.writeFileSync(convFilePath, jsonContent, 'utf-8'); - - // Also save with sessionId if provided (for compatibility with CLI session/load) - if (sessionId) { - const sessionFilename = `checkpoint-${sessionId}.json`; - const sessionFilePath = path.join(projectDir, sessionFilename); - console.log( - '[QwenSessionManager] Also saving checkpoint with sessionId:', - sessionFilePath, - ); - fs.writeFileSync(sessionFilePath, jsonContent, 'utf-8'); - } - - // Verify primary file exists - if (fs.existsSync(convFilePath)) { - const stats = fs.statSync(convFilePath); - console.log( - '[QwenSessionManager] Primary checkpoint verified, size:', - stats.size, - ); - } else { - console.error( - '[QwenSessionManager] ERROR: Primary checkpoint does not exist after write!', - ); - } - - console.log('[QwenSessionManager] ===== CHECKPOINT SAVED ====='); - console.log('[QwenSessionManager] Primary path:', convFilePath); - if (sessionId) { - console.log( - '[QwenSessionManager] Secondary path (sessionId):', - path.join(projectDir, `checkpoint-${sessionId}.json`), - ); - } - return conversationId; - } catch (error) { - console.error('[QwenSessionManager] ===== CHECKPOINT SAVE FAILED ====='); - console.error('[QwenSessionManager] Error:', error); - console.error( - '[QwenSessionManager] Error stack:', - error instanceof Error ? error.stack : 'N/A', - ); - throw error; - } - } - - /** - * Save current conversation as a named session (checkpoint-like functionality) + * Save current conversation as a named session * * @param messages - Current conversation messages * @param sessionName - Name/tag for the saved session diff --git a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts index 6e2d065d..3fc4e484 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionReader.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionReader.ts @@ -7,6 +7,8 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as readline from 'readline'; +import * as crypto from 'crypto'; export interface QwenMessage { id: string; @@ -32,6 +34,9 @@ export interface QwenSession { lastUpdated: string; messages: QwenMessage[]; filePath?: string; + messageCount?: number; + firstUserText?: string; + cwd?: string; } export class QwenSessionReader { @@ -96,11 +101,17 @@ export class QwenSessionReader { return sessions; } - const files = fs - .readdirSync(chatsDir) - .filter((f) => f.startsWith('session-') && f.endsWith('.json')); + const files = fs.readdirSync(chatsDir); - for (const file of files) { + const jsonSessionFiles = files.filter( + (f) => f.startsWith('session-') && f.endsWith('.json'), + ); + + const jsonlSessionFiles = files.filter((f) => + /^[0-9a-fA-F-]{32,36}\.jsonl$/.test(f), + ); + + for (const file of jsonSessionFiles) { const filePath = path.join(chatsDir, file); try { const content = fs.readFileSync(filePath, 'utf-8'); @@ -116,6 +127,23 @@ export class QwenSessionReader { } } + // Support new JSONL session format produced by the CLI + for (const file of jsonlSessionFiles) { + const filePath = path.join(chatsDir, file); + try { + const session = await this.readJsonlSession(filePath, false); + if (session) { + sessions.push(session); + } + } catch (error) { + console.error( + '[QwenSessionReader] Failed to read JSONL session file:', + filePath, + error, + ); + } + } + return sessions; } @@ -128,7 +156,25 @@ export class QwenSessionReader { ): Promise { // First try to find in all projects const sessions = await this.getAllSessions(undefined, true); - return sessions.find((s) => s.sessionId === sessionId) || null; + const found = sessions.find((s) => s.sessionId === sessionId); + + if (!found) { + return null; + } + + // If the session points to a JSONL file, load full content on demand + if ( + found.filePath && + found.filePath.endsWith('.jsonl') && + found.messages.length === 0 + ) { + const hydrated = await this.readJsonlSession(found.filePath, true); + if (hydrated) { + return hydrated; + } + } + + return found; } /** @@ -136,7 +182,6 @@ export class QwenSessionReader { * Qwen CLI uses SHA256 hash of project path */ private async getProjectHash(workingDir: string): Promise { - const crypto = await import('crypto'); return crypto.createHash('sha256').update(workingDir).digest('hex'); } @@ -144,6 +189,14 @@ export class QwenSessionReader { * Get session title (based on first user message) */ getSessionTitle(session: QwenSession): string { + // Prefer cached prompt text to avoid loading messages for JSONL sessions + if (session.firstUserText) { + return ( + session.firstUserText.substring(0, 50) + + (session.firstUserText.length > 50 ? '...' : '') + ); + } + const firstUserMessage = session.messages.find((m) => m.type === 'user'); if (firstUserMessage) { // Extract first 50 characters as title @@ -155,6 +208,137 @@ export class QwenSessionReader { return 'Untitled Session'; } + /** + * Parse a JSONL session file written by the CLI. + * When includeMessages is false, only lightweight metadata is returned. + */ + private async readJsonlSession( + filePath: string, + includeMessages: boolean, + ): Promise { + try { + if (!fs.existsSync(filePath)) { + return null; + } + + const stats = fs.statSync(filePath); + const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' }); + const rl = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity, + }); + + const messages: QwenMessage[] = []; + const seenUuids = new Set(); + let sessionId: string | undefined; + let startTime: string | undefined; + let firstUserText: string | undefined; + let cwd: string | undefined; + + for await (const line of rl) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + + let obj: Record; + try { + obj = JSON.parse(trimmed) as Record; + } catch { + continue; + } + + if (!sessionId && typeof obj.sessionId === 'string') { + sessionId = obj.sessionId; + } + if (!startTime && typeof obj.timestamp === 'string') { + startTime = obj.timestamp; + } + if (!cwd && typeof obj.cwd === 'string') { + cwd = obj.cwd; + } + + const type = typeof obj.type === 'string' ? obj.type : ''; + if (type === 'user' || type === 'assistant') { + const uuid = typeof obj.uuid === 'string' ? obj.uuid : undefined; + if (uuid) { + seenUuids.add(uuid); + } + + const text = this.contentToText(obj.message); + if (includeMessages) { + messages.push({ + id: uuid || `${messages.length}`, + timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : '', + type: type === 'user' ? 'user' : 'qwen', + content: text, + }); + } + + if (!firstUserText && type === 'user' && text) { + firstUserText = text; + } + } + } + + // Ensure stream is closed + rl.close(); + + if (!sessionId) { + return null; + } + + const projectHash = cwd + ? await this.getProjectHash(cwd) + : path.basename(path.dirname(path.dirname(filePath))); + + return { + sessionId, + projectHash, + startTime: startTime || new Date(stats.birthtimeMs).toISOString(), + lastUpdated: new Date(stats.mtimeMs).toISOString(), + messages: includeMessages ? messages : [], + filePath, + messageCount: seenUuids.size, + firstUserText, + cwd, + }; + } catch (error) { + console.error( + '[QwenSessionReader] Failed to parse JSONL session:', + error, + ); + return null; + } + } + + // Extract plain text from CLI Content structure + private contentToText(message: unknown): string { + try { + if (typeof message !== 'object' || message === null) { + return ''; + } + + const typed = message as { parts?: unknown[] }; + const parts = Array.isArray(typed.parts) ? typed.parts : []; + const texts: string[] = []; + for (const part of parts) { + if (typeof part !== 'object' || part === null) { + continue; + } + const p = part as Record; + if (typeof p.text === 'string') { + texts.push(p.text); + } else if (typeof p.data === 'string') { + texts.push(p.data); + } + } + return texts.join('\n'); + } catch { + return ''; + } + } + /** * Delete session file */ diff --git a/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts b/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts index e27fbe67..d7b24bb2 100644 --- a/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts +++ b/packages/vscode-ide-companion/src/services/qwenSessionUpdateHandler.ts @@ -10,7 +10,8 @@ * Handles session updates from ACP and dispatches them to appropriate callbacks */ -import type { AcpSessionUpdate, ApprovalModeValue } from '../types/acpTypes.js'; +import type { AcpSessionUpdate } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { QwenAgentCallbacks } from '../types/chatTypes.js'; /** diff --git a/packages/vscode-ide-companion/src/types/acpTypes.ts b/packages/vscode-ide-companion/src/types/acpTypes.ts index 1fb4de17..5ddbfd06 100644 --- a/packages/vscode-ide-companion/src/types/acpTypes.ts +++ b/packages/vscode-ide-companion/src/types/acpTypes.ts @@ -3,6 +3,7 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ +import type { ApprovalModeValue } from './approvalModeValueTypes.js'; export const JSONRPC_VERSION = '2.0' as const; export const authMethod = 'qwen-oauth'; @@ -138,8 +139,6 @@ export interface PlanUpdate extends BaseSessionUpdate { }; } -export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo'; - export { ApprovalMode, APPROVAL_MODE_MAP, @@ -167,6 +166,13 @@ export interface CurrentModeUpdate extends BaseSessionUpdate { }; } +// Authenticate update (sent by agent during authentication process) +export interface AuthenticateUpdateNotification { + _meta: { + authUri: string; + }; +} + export type AcpSessionUpdate = | UserMessageChunkUpdate | AgentMessageChunkUpdate diff --git a/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts new file mode 100644 index 00000000..fe1f37e1 --- /dev/null +++ b/packages/vscode-ide-companion/src/types/approvalModeValueTypes.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Type for approval mode values + * Used in ACP protocol for controlling agent behavior + */ +export type ApprovalModeValue = 'plan' | 'default' | 'auto-edit' | 'yolo'; diff --git a/packages/vscode-ide-companion/src/types/chatTypes.ts b/packages/vscode-ide-companion/src/types/chatTypes.ts index 90ebbb87..4cffd4eb 100644 --- a/packages/vscode-ide-companion/src/types/chatTypes.ts +++ b/packages/vscode-ide-companion/src/types/chatTypes.ts @@ -3,7 +3,8 @@ * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ -import type { AcpPermissionRequest, ApprovalModeValue } from './acpTypes.js'; +import type { AcpPermissionRequest } from './acpTypes.js'; +import type { ApprovalModeValue } from './approvalModeValueTypes.js'; export interface ChatMessage { role: 'user' | 'assistant'; @@ -34,7 +35,7 @@ export interface QwenAgentCallbacks { onToolCall?: (update: ToolCallUpdateData) => void; onPlan?: (entries: PlanEntry[]) => void; onPermissionRequest?: (request: AcpPermissionRequest) => Promise; - onEndTurn?: () => void; + onEndTurn?: (reason?: string) => void; onModeInfo?: (info: { currentModeId?: ApprovalModeValue; availableModes?: Array<{ diff --git a/packages/vscode-ide-companion/src/types/connectionTypes.ts b/packages/vscode-ide-companion/src/types/connectionTypes.ts index b49bd027..7ada3aed 100644 --- a/packages/vscode-ide-companion/src/types/connectionTypes.ts +++ b/packages/vscode-ide-companion/src/types/connectionTypes.ts @@ -5,7 +5,11 @@ */ import type { ChildProcess } from 'child_process'; -import type { AcpSessionUpdate, AcpPermissionRequest } from './acpTypes.js'; +import type { + AcpSessionUpdate, + AcpPermissionRequest, + AuthenticateUpdateNotification, +} from './acpTypes.js'; export interface PendingRequest { resolve: (value: T) => void; @@ -19,7 +23,8 @@ export interface AcpConnectionCallbacks { onPermissionRequest: (data: AcpPermissionRequest) => Promise<{ optionId: string; }>; - onEndTurn: () => void; + onAuthenticateUpdate: (data: AuthenticateUpdateNotification) => void; + onEndTurn: (reason?: string) => void; } export interface AcpConnectionState { diff --git a/packages/vscode-ide-companion/src/utils/authErrors.ts b/packages/vscode-ide-companion/src/utils/authErrors.ts new file mode 100644 index 00000000..8b0e6af9 --- /dev/null +++ b/packages/vscode-ide-companion/src/utils/authErrors.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +const AUTH_ERROR_PATTERNS = [ + 'Authentication required', // Standard authentication request message + '(code: -32000)', // RPC error code -32000 indicates authentication failure + 'Unauthorized', // HTTP unauthorized error + 'Invalid token', // Invalid token + 'Session expired', // Session expired +]; + +/** + * Determines if the given error is authentication-related + */ +export const isAuthenticationRequiredError = (error: unknown): boolean => { + // Null check to avoid unnecessary processing + if (!error) { + return false; + } + + // Extract error message text + const message = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : String(error); + + // Match authentication-related errors using predefined patterns + return AUTH_ERROR_PATTERNS.some((pattern) => message.includes(pattern)); +}; diff --git a/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts new file mode 100644 index 00000000..362867c2 --- /dev/null +++ b/packages/vscode-ide-companion/src/utils/authNotificationHandler.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode'; +import type { AuthenticateUpdateNotification } from '../types/acpTypes.js'; + +// Store reference to the current notification +let currentNotification: Thenable | null = null; + +/** + * Handle authentication update notifications by showing a VS Code notification + * with the authentication URI and action buttons. + * + * @param data - Authentication update notification data containing the auth URI + */ +export function handleAuthenticateUpdate( + data: AuthenticateUpdateNotification, +): void { + const authUri = data._meta.authUri; + + // Store reference to the current notification + currentNotification = vscode.window.showInformationMessage( + `Qwen Code needs authentication. Click an action below:`, + 'Open in Browser', + 'Copy Link', + 'Dismiss', + ); + + currentNotification.then((selection) => { + if (selection === 'Open in Browser') { + // Open the authentication URI in the default browser + vscode.env.openExternal(vscode.Uri.parse(authUri)); + vscode.window.showInformationMessage( + 'Opening authentication page in your browser...', + ); + } else if (selection === 'Copy Link') { + // Copy the authentication URI to clipboard + vscode.env.clipboard.writeText(authUri); + vscode.window.showInformationMessage( + 'Authentication link copied to clipboard!', + ); + } + + // Clear the notification reference after user interaction + currentNotification = null; + }); +} diff --git a/packages/vscode-ide-companion/src/webview/App.tsx b/packages/vscode-ide-companion/src/webview/App.tsx index 4b51d6b6..5eacdabf 100644 --- a/packages/vscode-ide-companion/src/webview/App.tsx +++ b/packages/vscode-ide-companion/src/webview/App.tsx @@ -29,6 +29,7 @@ import { PermissionDrawer } from './components/PermissionDrawer/PermissionDrawer import { ToolCall } from './components/messages/toolcalls/ToolCall.js'; import { hasToolCallOutput } from './components/messages/toolcalls/shared/utils.js'; import { EmptyState } from './components/layout/EmptyState.js'; +import { Onboarding } from './components/layout/Onboarding.js'; import { type CompletionItem } from '../types/completionItemTypes.js'; import { useCompletionTrigger } from './hooks/useCompletionTrigger.js'; import { ChatHeader } from './components/layout/ChatHeader.js'; @@ -43,7 +44,7 @@ import { InputForm } from './components/layout/InputForm.js'; import { SessionSelector } from './components/layout/SessionSelector.js'; import { FileIcon, UserIcon } from './components/icons/index.js'; import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js'; -import type { ApprovalModeValue } from '../types/acpTypes.js'; +import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js'; import type { PlanEntry } from '../types/chatTypes.js'; export const App: React.FC = () => { @@ -67,6 +68,8 @@ export const App: React.FC = () => { toolCall: PermissionToolCall; } | null>(null); const [planEntries, setPlanEntries] = useState([]); + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [isLoading, setIsLoading] = useState(true); // Track if we're still initializing/loading const messagesEndRef = useRef( null, ) as React.RefObject; @@ -90,9 +93,13 @@ export const App: React.FC = () => { const getCompletionItems = React.useCallback( async (trigger: '@' | '/', query: string): Promise => { if (trigger === '@') { - if (!fileContext.hasRequestedFiles) { - fileContext.requestWorkspaceFiles(); - } + console.log('[App] getCompletionItems @ called', { + query, + requested: fileContext.hasRequestedFiles, + workspaceFiles: fileContext.workspaceFiles.length, + }); + // 始终根据当前 query 触发请求,让 hook 判断是否需要真正请求 + fileContext.requestWorkspaceFiles(query); const fileIcon = ; const allItems: CompletionItem[] = fileContext.workspaceFiles.map( @@ -109,7 +116,6 @@ export const App: React.FC = () => { ); if (query && query.length >= 1) { - fileContext.requestWorkspaceFiles(query); const lowerQuery = query.toLowerCase(); return allItems.filter( (item) => @@ -154,20 +160,42 @@ export const App: React.FC = () => { const completion = useCompletionTrigger(inputFieldRef, getCompletionItems); + // Track a lightweight signature of workspace files to detect content changes even when length is unchanged + const workspaceFilesSignature = useMemo( + () => + fileContext.workspaceFiles + .map( + (file) => + `${file.id}|${file.label}|${file.description ?? ''}|${file.path}`, + ) + .join('||'), + [fileContext.workspaceFiles], + ); + // When workspace files update while menu open for @, refresh items so the first @ shows the list // Note: Avoid depending on the entire `completion` object here, since its identity // changes on every render which would retrigger this effect and can cause a refresh loop. useEffect(() => { - if (completion.isOpen && completion.triggerChar === '@') { + // Only auto-refresh when there's no query (first @ popup) to avoid repeated refreshes during search + if ( + completion.isOpen && + completion.triggerChar === '@' && + !completion.query + ) { // Only refresh items; do not change other completion state to avoid re-renders loops completion.refreshCompletion(); } // Only re-run when the actual data source changes, not on every render // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fileContext.workspaceFiles, completion.isOpen, completion.triggerChar]); + }, [ + workspaceFilesSignature, + completion.isOpen, + completion.triggerChar, + completion.query, + ]); // Message submission - const handleSubmit = useMessageSubmit({ + const { handleSubmit: submitMessage } = useMessageSubmit({ inputText, setInputText, messageHandling, @@ -176,6 +204,7 @@ export const App: React.FC = () => { vscode, inputFieldRef, isStreaming: messageHandling.isStreaming, + isWaitingForResponse: messageHandling.isWaitingForResponse, }); // Handle cancel/stop from the input bar @@ -218,6 +247,7 @@ export const App: React.FC = () => { inputFieldRef, setInputText, setEditMode, + setIsAuthenticated, }); // Auto-scroll handling: keep the view pinned to bottom when new content arrives, @@ -331,6 +361,14 @@ export const App: React.FC = () => { completedToolCalls, ]); + // Set loading state to false after initial mount and when we have authentication info + useEffect(() => { + // If we have determined authentication status, we're done loading + if (isAuthenticated !== null) { + setIsLoading(false); + } + }, [isAuthenticated]); + // Handle permission response const handlePermissionResponse = useCallback( (optionId: string) => { @@ -487,6 +525,22 @@ export const App: React.FC = () => { setThinkingEnabled((prev) => !prev); }; + // When user sends a message after scrolling up, re-pin and jump to the bottom + const handleSubmitWithScroll = useCallback( + (e: React.FormEvent) => { + setPinnedToBottom(true); + + const container = messagesContainerRef.current; + if (container) { + const top = container.scrollHeight - container.clientHeight; + container.scrollTo({ top }); + } + + submitMessage(e); + }, + [submitMessage], + ); + // Create unified message array containing all types of messages and tool calls const allMessages = useMemo< Array<{ @@ -621,7 +675,19 @@ export const App: React.FC = () => { allMessages.length > 0; return ( -
+
+ {/* Top-level loading overlay */} + {isLoading && ( +
+
+
+

+ Preparing Qwen Code... +

+
+
+ )} + {
- {!hasContent ? ( - + {!hasContent && !isLoading ? ( + isAuthenticated === false ? ( + { + vscode.postMessage({ type: 'login', data: {} }); + messageHandling.setWaitingForResponse( + 'Logging in to Qwen Code...', + ); + }} + /> + ) : isAuthenticated === null ? ( + + ) : ( + + ) ) : ( <> {/* Render all messages and tool calls */} {renderMessages()} - {/* Flow-in persistent slot: keeps a small constant height so toggling */} - {/* the waiting message doesn't change list height to zero. When */} - {/* active, render the waiting message inline (not fixed). */} -
- {messageHandling.isWaitingForResponse && - messageHandling.loadingMessage && ( + + {/* Waiting message positioned fixed above the input form to avoid layout shifts */} + {messageHandling.isWaitingForResponse && + messageHandling.loadingMessage && ( +
- )} -
- +
+ )}
)}
- setIsComposing(true)} - onCompositionEnd={() => setIsComposing(false)} - onKeyDown={() => {}} - onSubmit={handleSubmit.handleSubmit} - onCancel={handleCancel} - onToggleEditMode={handleToggleEditMode} - onToggleThinking={handleToggleThinking} - onFocusActiveEditor={fileContext.focusActiveEditor} - onToggleSkipAutoActiveContext={() => - setSkipAutoActiveContext((v) => !v) - } - onShowCommandMenu={async () => { - if (inputFieldRef.current) { - inputFieldRef.current.focus(); + {isAuthenticated && ( + setIsComposing(true)} + onCompositionEnd={() => setIsComposing(false)} + onKeyDown={() => {}} + onSubmit={handleSubmitWithScroll} + onCancel={handleCancel} + onToggleEditMode={handleToggleEditMode} + onToggleThinking={handleToggleThinking} + onFocusActiveEditor={fileContext.focusActiveEditor} + onToggleSkipAutoActiveContext={() => + setSkipAutoActiveContext((v) => !v) + } + onShowCommandMenu={async () => { + if (inputFieldRef.current) { + inputFieldRef.current.focus(); - const selection = window.getSelection(); - let position = { top: 0, left: 0 }; + const selection = window.getSelection(); + let position = { top: 0, left: 0 }; - if (selection && selection.rangeCount > 0) { - try { - const range = selection.getRangeAt(0); - const rangeRect = range.getBoundingClientRect(); - if (rangeRect.top > 0 && rangeRect.left > 0) { - position = { - top: rangeRect.top, - left: rangeRect.left, - }; - } else { + if (selection && selection.rangeCount > 0) { + try { + const range = selection.getRangeAt(0); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.top > 0 && rangeRect.left > 0) { + position = { + top: rangeRect.top, + left: rangeRect.left, + }; + } else { + const inputRect = + inputFieldRef.current.getBoundingClientRect(); + position = { top: inputRect.top, left: inputRect.left }; + } + } catch (error) { + console.error('[App] Error getting cursor position:', error); const inputRect = inputFieldRef.current.getBoundingClientRect(); position = { top: inputRect.top, left: inputRect.left }; } - } catch (error) { - console.error('[App] Error getting cursor position:', error); + } else { const inputRect = inputFieldRef.current.getBoundingClientRect(); position = { top: inputRect.top, left: inputRect.left }; } - } else { - const inputRect = inputFieldRef.current.getBoundingClientRect(); - position = { top: inputRect.top, left: inputRect.left }; + + await completion.openCompletion('/', '', position); } + }} + onAttachContext={handleAttachContextClick} + completionIsOpen={completion.isOpen} + completionItems={completion.items} + onCompletionSelect={handleCompletionSelect} + onCompletionClose={completion.closeCompletion} + /> + )} - await completion.openCompletion('/', '', position); - } - }} - onAttachContext={handleAttachContextClick} - completionIsOpen={completion.isOpen} - completionItems={completion.items} - onCompletionSelect={handleCompletionSelect} - onCompletionClose={completion.closeCompletion} - /> - - {permissionRequest && ( + {isAuthenticated && permissionRequest && ( { // Panel dispose callback this.disposables.forEach((d) => d.dispose()); @@ -122,12 +118,15 @@ export class WebViewProvider { }); }); - // Setup end-turn handler from ACP stopReason=end_turn - this.agentManager.onEndTurn(() => { + // Setup end-turn handler from ACP stopReason notifications + this.agentManager.onEndTurn((reason) => { // Ensure WebView exits streaming state even if no explicit streamEnd was emitted elsewhere this.sendMessageToWebView({ type: 'streamEnd', - data: { timestamp: Date.now(), reason: 'end_turn' }, + data: { + timestamp: Date.now(), + reason: reason || 'end_turn', + }, }); }); @@ -522,40 +521,14 @@ export class WebViewProvider { */ private async attemptAuthStateRestoration(): Promise { try { - if (this.authStateManager) { - // Debug current auth state - await this.authStateManager.debugAuthState(); - - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - const hasValidAuth = await this.authStateManager.hasValidAuth( - workingDir, - authMethod, - ); - console.log('[WebViewProvider] Has valid cached auth:', hasValidAuth); - - if (hasValidAuth) { - console.log( - '[WebViewProvider] Valid auth found, attempting connection...', - ); - // Try to connect with cached auth - await this.initializeAgentConnection(); - } else { - console.log( - '[WebViewProvider] No valid auth found, rendering empty conversation', - ); - // Render the chat UI immediately without connecting - await this.initializeEmptyConversation(); - } - } else { - console.log( - '[WebViewProvider] No auth state manager, rendering empty conversation', - ); - await this.initializeEmptyConversation(); - } - } catch (_error) { - console.error('[WebViewProvider] Auth state restoration failed:', _error); - // Fallback to rendering empty conversation + console.log('[WebViewProvider] Attempting connection...'); + // Attempt a connection to detect prior auth without forcing login + await this.initializeAgentConnection({ autoAuthenticate: false }); + } catch (error) { + console.error( + '[WebViewProvider] Error in attemptAuthStateRestoration:', + error, + ); await this.initializeEmptyConversation(); } } @@ -564,70 +537,88 @@ export class WebViewProvider { * Initialize agent connection and session * Can be called from show() or via /login command */ - async initializeAgentConnection(): Promise { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + async initializeAgentConnection(options?: { + autoAuthenticate?: boolean; + }): Promise { + return this.doInitializeAgentConnection(options); + } - console.log( - '[WebViewProvider] Starting initialization, workingDir:', - workingDir, - ); - console.log( - '[WebViewProvider] AuthStateManager available:', - !!this.authStateManager, - ); + /** + * Internal: perform actual connection/initialization (no auth locking). + */ + private async doInitializeAgentConnection(options?: { + autoAuthenticate?: boolean; + }): Promise { + const autoAuthenticate = options?.autoAuthenticate ?? true; + const run = async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - // Check if CLI is installed before attempting to connect - const cliDetection = await CliDetector.detectQwenCli(); - - if (!cliDetection.isInstalled) { console.log( - '[WebViewProvider] Qwen CLI not detected, skipping agent connection', + '[WebViewProvider] Starting initialization, workingDir:', + workingDir, ); - console.log('[WebViewProvider] CLI detection error:', cliDetection.error); - - // Show VSCode notification with installation option - await CliInstaller.promptInstallation(); - - // Initialize empty conversation (can still browse history) - await this.initializeEmptyConversation(); - } else { console.log( - '[WebViewProvider] Qwen CLI detected, attempting connection...', + `[WebViewProvider] Using CLI-managed authentication (autoAuth=${autoAuthenticate})`, ); - console.log('[WebViewProvider] CLI path:', cliDetection.cliPath); - console.log('[WebViewProvider] CLI version:', cliDetection.version); + + const bundledCliEntry = vscode.Uri.joinPath( + this.extensionUri, + 'dist', + 'qwen-cli', + 'cli.js', + ).fsPath; try { console.log('[WebViewProvider] Connecting to agent...'); - console.log( - '[WebViewProvider] Using authStateManager:', - !!this.authStateManager, - ); - const authInfo = await this.authStateManager.getAuthInfo(); - console.log('[WebViewProvider] Auth cache status:', authInfo); // Pass the detected CLI path to ensure we use the correct installation - await this.agentManager.connect( + const connectResult = await this.agentManager.connect( workingDir, - this.authStateManager, - cliDetection.cliPath, + bundledCliEntry, + options, ); console.log('[WebViewProvider] Agent connected successfully'); this.agentInitialized = true; - // Load messages from the current Qwen session - await this.loadCurrentSessionMessages(); + // If authentication is required and autoAuthenticate is false, + // send authState message and return without creating session + if (connectResult.requiresAuth && !autoAuthenticate) { + console.log( + '[WebViewProvider] Authentication required but auto-auth disabled, sending authState and returning', + ); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + // Initialize empty conversation to allow browsing history + await this.initializeEmptyConversation(); + return; + } - // Notify webview that agent is connected - this.sendMessageToWebView({ - type: 'agentConnected', - data: {}, - }); + if (connectResult.requiresAuth) { + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + } + + // Load messages from the current Qwen session + const sessionReady = await this.loadCurrentSessionMessages(options); + + if (sessionReady) { + // Notify webview that agent is connected + this.sendMessageToWebView({ + type: 'agentConnected', + data: {}, + }); + } else { + console.log( + '[WebViewProvider] Session creation deferred until user logs in.', + ); + } } catch (_error) { console.error('[WebViewProvider] Agent connection error:', _error); - // Clear auth cache on error (might be auth issue) - await this.authStateManager.clearAuthState(); vscode.window.showWarningMessage( `Failed to connect to Qwen CLI: ${_error}\nYou can still use the chat UI, but messages won't be sent to AI.`, ); @@ -642,7 +633,9 @@ export class WebViewProvider { }, }); } - } + }; + + return run(); } /** @@ -651,29 +644,16 @@ export class WebViewProvider { */ async forceReLogin(): Promise { console.log('[WebViewProvider] Force re-login requested'); - console.log( - '[WebViewProvider] Current authStateManager:', - !!this.authStateManager, - ); - await vscode.window.withProgress( + return vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, - title: 'Logging in to Qwen Code... ', cancellable: false, }, async (progress) => { try { progress.report({ message: 'Preparing sign-in...' }); - // Clear existing auth cache - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - console.log('[WebViewProvider] Auth cache cleared'); - } else { - console.log('[WebViewProvider] No authStateManager to clear'); - } - // Disconnect existing connection if any if (this.agentInitialized) { try { @@ -693,19 +673,11 @@ export class WebViewProvider { }); // Reinitialize connection (will trigger fresh authentication) - await this.initializeAgentConnection(); + await this.doInitializeAgentConnection({ autoAuthenticate: true }); console.log( '[WebViewProvider] Force re-login completed successfully', ); - // Ensure auth state is saved after successful re-login - if (this.authStateManager) { - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - await this.authStateManager.saveAuthState(workingDir, authMethod); - console.log('[WebViewProvider] Auth state saved after re-login'); - } - // Send success notification to WebView this.sendMessageToWebView({ type: 'loginSuccess', @@ -784,7 +756,11 @@ export class WebViewProvider { * Load messages from current Qwen session * Skips session restoration and creates a new session directly */ - private async loadCurrentSessionMessages(): Promise { + private async loadCurrentSessionMessages(options?: { + autoAuthenticate?: boolean; + }): Promise { + const autoAuthenticate = options?.autoAuthenticate ?? true; + let sessionReady = false; try { console.log( '[WebViewProvider] Initializing with new session (skipping restoration)', @@ -793,29 +769,49 @@ export class WebViewProvider { const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); - // Skip session restoration entirely and create a new session directly - try { - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); - console.log('[WebViewProvider] ACP session created successfully'); - - // Ensure auth state is saved after successful session creation - if (this.authStateManager) { - await this.authStateManager.saveAuthState(workingDir, authMethod); + // avoid creating another session if connect() already created one. + if (!this.agentManager.currentSessionId) { + if (!autoAuthenticate) { console.log( - '[WebViewProvider] Auth state saved after session creation', + '[WebViewProvider] Skipping ACP session creation until user logs in.', ); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + } else { + try { + await this.agentManager.createNewSession(workingDir, { + autoAuthenticate, + }); + console.log('[WebViewProvider] ACP session created successfully'); + sessionReady = true; + } catch (sessionError) { + const requiresAuth = isAuthenticationRequiredError(sessionError); + if (requiresAuth && !autoAuthenticate) { + console.log( + '[WebViewProvider] ACP session requires authentication; waiting for explicit login.', + ); + this.sendMessageToWebView({ + type: 'authState', + data: { authenticated: false }, + }); + } else { + console.error( + '[WebViewProvider] Failed to create ACP session:', + sessionError, + ); + vscode.window.showWarningMessage( + `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, + ); + } + } } - } catch (sessionError) { - console.error( - '[WebViewProvider] Failed to create ACP session:', - sessionError, - ); - vscode.window.showWarningMessage( - `Failed to create ACP session: ${sessionError}. You may need to authenticate first.`, + } else { + console.log( + '[WebViewProvider] Existing ACP session detected, skipping new session creation', ); + sessionReady = true; } await this.initializeEmptyConversation(); @@ -828,7 +824,10 @@ export class WebViewProvider { `Failed to load session messages: ${_error}`, ); await this.initializeEmptyConversation(); + return false; } + + return sessionReady; } /** @@ -974,17 +973,6 @@ export class WebViewProvider { this.agentManager.disconnect(); } - /** - * Clear authentication cache for this WebViewProvider instance - */ - async clearAuthCache(): Promise { - console.log('[WebViewProvider] Clearing auth cache for this instance'); - if (this.authStateManager) { - await this.authStateManager.clearAuthState(); - this.resetAgentState(); - } - } - /** * Restore an existing WebView panel (called during VSCode restart) * This sets up the panel with all event listeners @@ -992,8 +980,7 @@ export class WebViewProvider { async restorePanel(panel: vscode.WebviewPanel): Promise { console.log('[WebViewProvider] Restoring WebView panel'); console.log( - '[WebViewProvider] Current authStateManager in restore:', - !!this.authStateManager, + '[WebViewProvider] Using CLI-managed authentication in restore', ); this.panelManager.setPanel(panel); @@ -1196,18 +1183,13 @@ export class WebViewProvider { const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); // Create new Qwen session via agent manager - await this.agentManager.createNewSession( - workingDir, - this.authStateManager, - ); + await this.agentManager.createNewSession(workingDir); // Clear current conversation UI this.sendMessageToWebView({ type: 'conversationCleared', data: {}, }); - - console.log('[WebViewProvider] New session created successfully'); } catch (_error) { console.error('[WebViewProvider] Failed to create new session:', _error); vscode.window.showErrorMessage(`Failed to create new session: ${_error}`); diff --git a/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx b/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx index 167a376d..f667b849 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/CompletionMenu.tsx @@ -92,9 +92,8 @@ export const CompletionMenu: React.FC = ({ ref={containerRef} role="menu" className={[ - // Semantic class name for readability (no CSS attached) 'completion-menu', - // Positioning and container styling (Tailwind) + // Positioning and container styling 'absolute bottom-full left-0 right-0 mb-2 flex flex-col overflow-hidden', 'rounded-large border bg-[var(--app-menu-background)]', 'border-[var(--app-input-border)] max-h-[50vh] z-[1000]', diff --git a/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx b/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx index 081352b8..1b424e24 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/EmptyState.tsx @@ -7,24 +7,56 @@ import type React from 'react'; import { generateIconUrl } from '../../utils/resourceUrl.js'; -export const EmptyState: React.FC = () => { +interface EmptyStateProps { + isAuthenticated?: boolean; + loadingMessage?: string; +} + +export const EmptyState: React.FC = ({ + isAuthenticated = false, + loadingMessage, +}) => { // Generate icon URL using the utility function const iconUri = generateIconUrl('icon.png'); + const description = loadingMessage + ? 'Preparing Qwen Code…' + : isAuthenticated + ? 'What would you like to do? Ask about this codebase or we can start writing code.' + : 'Welcome! Please log in to start using Qwen Code.'; + return (
{/* Qwen Logo */}
- Qwen Logo + {iconUri ? ( + Qwen Logo { + // Fallback to a div with text if image fails to load + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + const parent = target.parentElement; + if (parent) { + const fallback = document.createElement('div'); + fallback.className = + 'w-[60px] h-[60px] flex items-center justify-center text-2xl font-bold'; + fallback.textContent = 'Q'; + parent.appendChild(fallback); + } + }} + /> + ) : ( +
+ Q +
+ )}
- What to do first? Ask about this codebase or we can start writing - code. + {description}
diff --git a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx index fe86ea99..86ba42be 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/InputForm.tsx @@ -11,7 +11,7 @@ import { PlanModeIcon, CodeBracketsIcon, HideContextIcon, - ThinkingIcon, + // ThinkingIcon, // Temporarily disabled SlashCommandIcon, LinkIcon, ArrowUpIcon, @@ -20,7 +20,7 @@ import { import { CompletionMenu } from '../layout/CompletionMenu.js'; import type { CompletionItem } from '../../../types/completionItemTypes.js'; import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js'; -import type { ApprovalModeValue } from '../../../types/acpTypes.js'; +import type { ApprovalModeValue } from '../../../types/approvalModeValueTypes.js'; interface InputFormProps { inputText: string; @@ -92,7 +92,7 @@ export const InputForm: React.FC = ({ isWaitingForResponse, isComposing, editMode, - thinkingEnabled, + // thinkingEnabled, // Temporarily disabled activeFileName, activeSelection, skipAutoActiveContext, @@ -103,7 +103,7 @@ export const InputForm: React.FC = ({ onSubmit, onCancel, onToggleEditMode, - onToggleThinking, + // onToggleThinking, // Temporarily disabled onToggleSkipAutoActiveContext, onShowCommandMenu, onAttachContext, @@ -113,6 +113,7 @@ export const InputForm: React.FC = ({ onCompletionClose, }) => { const editModeInfo = getEditModeInfo(editMode); + const composerDisabled = isStreaming || isWaitingForResponse; const handleKeyDown = (e: React.KeyboardEvent) => { // ESC should cancel the current interaction (stop generation) @@ -144,7 +145,7 @@ export const InputForm: React.FC = ({ return (
@@ -179,10 +180,16 @@ export const InputForm: React.FC = ({ data-placeholder="Ask Qwen Code …" // Use a data flag so CSS can show placeholder even if the browser // inserts an invisible
into contentEditable (so :empty no longer matches) - data-empty={inputText.trim().length === 0 ? 'true' : 'false'} + data-empty={ + inputText.replace(/\u200B/g, '').trim().length === 0 + ? 'true' + : 'false' + } onInput={(e) => { const target = e.target as HTMLDivElement; - onInputChange(target.textContent || ''); + // Filter out zero-width space that we use to maintain height + const text = target.textContent?.replace(/\u200B/g, '') || ''; + onInputChange(text); }} onCompositionStart={onCompositionStart} onCompositionEnd={onCompositionEnd} @@ -236,15 +243,16 @@ export const InputForm: React.FC = ({ {/* Spacer */}
+ {/* @yiliang114. closed temporarily */} {/* Thinking button */} - + */} {/* Command button */} diff --git a/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx new file mode 100644 index 00000000..2eddc4d3 --- /dev/null +++ b/packages/vscode-ide-companion/src/webview/components/layout/Onboarding.tsx @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { generateIconUrl } from '../../utils/resourceUrl.js'; + +interface OnboardingPageProps { + onLogin: () => void; +} + +export const Onboarding: React.FC = ({ onLogin }) => { + const iconUri = generateIconUrl('icon.png'); + + return ( +
+
+
+ {/* Application icon container */} +
+ Qwen Code Logo +
+ +
+

+ Welcome to Qwen Code +

+

+ Unlock the power of AI to understand, navigate, and transform your + codebase faster than ever before. +

+
+ + +
+
+
+ ); +}; diff --git a/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx b/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx index ab7f6d51..1b744c1d 100644 --- a/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx +++ b/packages/vscode-ide-companion/src/webview/components/layout/SessionSelector.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; -import { groupSessionsByDate } from '../../utils/sessionGrouping.js'; -import { getTimeAgo } from '../../utils/timeUtils.js'; +import { + getTimeAgo, + groupSessionsByDate, +} from '../../utils/sessionGrouping.js'; import { SearchIcon } from '../icons/index.js'; interface SessionSelectorProps { diff --git a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx index ed8badcc..84712efa 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx +++ b/packages/vscode-ide-companion/src/webview/components/messages/Assistant/AssistantMessage.tsx @@ -75,7 +75,11 @@ export const AssistantMessage: React.FC = ({ whiteSpace: 'normal', }} > - +
diff --git a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts index 4ae9efd6..ceb2cb2b 100644 --- a/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts +++ b/packages/vscode-ide-companion/src/webview/components/messages/toolcalls/shared/utils.ts @@ -61,25 +61,6 @@ export const safeTitle = (title: unknown): string => { return ''; }; -/** - * Get icon emoji for a given tool kind - */ -export const getKindIcon = (kind: string): string => { - const kindMap: Record = { - edit: '✏️', - write: '✏️', - read: '📖', - execute: '⚡', - fetch: '🌐', - delete: '🗑️', - move: '📦', - search: '🔍', - think: '💭', - diff: '📝', - }; - return kindMap[kind.toLowerCase()] || '🔧'; -}; - /** * Check if a tool call should be displayed * Hides internal tool calls diff --git a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts index f82525f7..28ecbbd3 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/FileMessageHandler.ts @@ -49,7 +49,6 @@ export class FileMessageHandler extends BaseMessageHandler { break; case 'openDiff': - console.log('[FileMessageHandler ===== ] openDiff called with:', data); await this.handleOpenDiff(data); break; diff --git a/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts b/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts index adf94e29..353dbaaf 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/MessageRouter.ts @@ -11,7 +11,6 @@ import { SessionMessageHandler } from './SessionMessageHandler.js'; import { FileMessageHandler } from './FileMessageHandler.js'; import { EditorMessageHandler } from './EditorMessageHandler.js'; import { AuthMessageHandler } from './AuthMessageHandler.js'; -import { SettingsMessageHandler } from './SettingsMessageHandler.js'; /** * Message Router @@ -63,20 +62,12 @@ export class MessageRouter { sendToWebView, ); - const settingsHandler = new SettingsMessageHandler( - agentManager, - conversationStore, - currentConversationId, - sendToWebView, - ); - // Register handlers in order of priority this.handlers = [ this.sessionHandler, fileHandler, editorHandler, this.authHandler, - settingsHandler, ]; } @@ -159,11 +150,4 @@ export class MessageRouter { appendStreamContent(chunk: string): void { this.sessionHandler.appendStreamContent(chunk); } - - /** - * Check if saving checkpoint - */ - getIsSavingCheckpoint(): boolean { - return this.sessionHandler.getIsSavingCheckpoint(); - } } diff --git a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts index 741d9684..51dfbdd9 100644 --- a/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts +++ b/packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { BaseMessageHandler } from './BaseMessageHandler.js'; import type { ChatMessage } from '../../services/qwenAgentManager.js'; +import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; /** * Session message handler @@ -14,7 +15,6 @@ import type { ChatMessage } from '../../services/qwenAgentManager.js'; */ export class SessionMessageHandler extends BaseMessageHandler { private currentStreamContent = ''; - private isSavingCheckpoint = false; private loginHandler: (() => Promise) | null = null; private isTitleSet = false; // Flag to track if title has been set @@ -29,6 +29,8 @@ export class SessionMessageHandler extends BaseMessageHandler { 'cancelStreaming', // UI action: open a new chat tab (new WebviewPanel) 'openNewChatTab', + // Settings-related messages + 'setApprovalMode', ].includes(messageType); } @@ -112,6 +114,14 @@ export class SessionMessageHandler extends BaseMessageHandler { await this.handleCancelStreaming(); break; + case 'setApprovalMode': + await this.handleSetApprovalMode( + message.data as { + modeId?: ApprovalModeValue; + }, + ); + break; + default: console.warn( '[SessionMessageHandler] Unknown message type:', @@ -143,10 +153,47 @@ export class SessionMessageHandler extends BaseMessageHandler { } /** - * Check if saving checkpoint + * Prompt user to login and invoke the registered login handler/command. + * Returns true if a login was initiated. */ - getIsSavingCheckpoint(): boolean { - return this.isSavingCheckpoint; + private async promptLogin(message: string): Promise { + const result = await vscode.window.showWarningMessage(message, 'Login Now'); + if (result === 'Login Now') { + if (this.loginHandler) { + await this.loginHandler(); + } else { + await vscode.commands.executeCommand('qwen-code.login'); + } + return true; + } + return false; + } + + /** + * Prompt user to login or view offline. Returns 'login', 'offline', or 'dismiss'. + * When login is chosen, it triggers the login handler/command. + */ + private async promptLoginOrOffline( + message: string, + ): Promise<'login' | 'offline' | 'dismiss'> { + const selection = await vscode.window.showWarningMessage( + message, + 'Login Now', + 'View Offline', + ); + + if (selection === 'Login Now') { + if (this.loginHandler) { + await this.loginHandler(); + } else { + await vscode.commands.executeCommand('qwen-code.login'); + } + return 'login'; + } + if (selection === 'View Offline') { + return 'offline'; + } + return 'dismiss'; } /** @@ -271,26 +318,37 @@ export class SessionMessageHandler extends BaseMessageHandler { console.warn('[SessionMessageHandler] Agent not connected'); // Show non-modal notification with Login button - const result = await vscode.window.showWarningMessage( - 'You need to login first to use Qwen Code.', - 'Login Now', - ); - - if (result === 'Login Now') { - // Use login handler directly - if (this.loginHandler) { - await this.loginHandler(); - } else { - // Fallback to command - vscode.window.showInformationMessage( - 'Please wait while we connect to Qwen Code...', - ); - await vscode.commands.executeCommand('qwen-code.login'); - } - } + await this.promptLogin('You need to login first to use Qwen Code.'); return; } + // Ensure an ACP session exists before sending prompt + if (!this.agentManager.currentSessionId) { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); + await this.agentManager.createNewSession(workingDir); + } catch (createErr) { + console.error( + '[SessionMessageHandler] Failed to create session before sending message:', + createErr, + ); + const errorMsg = + createErr instanceof Error ? createErr.message : String(createErr); + if ( + errorMsg.includes('Authentication required') || + errorMsg.includes('(code: -32000)') + ) { + await this.promptLogin( + 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', + ); + return; + } + vscode.window.showErrorMessage(`Failed to create session: ${errorMsg}`); + return; + } + } + // Send to agent try { this.resetStreamContent(); @@ -319,41 +377,6 @@ export class SessionMessageHandler extends BaseMessageHandler { type: 'streamEnd', data: { timestamp: Date.now() }, }); - - // Auto-save checkpoint - if (this.currentConversationId) { - try { - const conversation = await this.conversationStore.getConversation( - this.currentConversationId, - ); - - const messages = conversation?.messages || []; - - this.isSavingCheckpoint = true; - - const result = await this.agentManager.saveCheckpoint( - messages, - this.currentConversationId, - ); - - setTimeout(() => { - this.isSavingCheckpoint = false; - }, 2000); - - if (result.success) { - console.log( - '[SessionMessageHandler] Checkpoint saved:', - result.tag, - ); - } - } catch (error) { - console.error( - '[SessionMessageHandler] Checkpoint save failed:', - error, - ); - this.isSavingCheckpoint = false; - } - } } catch (error) { console.error('[SessionMessageHandler] Error sending message:', error); @@ -391,19 +414,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('Invalid token') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to continue using Qwen Code.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -428,38 +442,14 @@ export class SessionMessageHandler extends BaseMessageHandler { // Ensure connection (login) before creating a new session if (!this.agentManager.isConnected) { - const result = await vscode.window.showWarningMessage( + const proceeded = await this.promptLogin( 'You need to login before creating a new session.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else { + if (!proceeded) { return; } } - // Save current session before creating new one - if (this.currentConversationId && this.agentManager.isConnected) { - try { - const conversation = await this.conversationStore.getConversation( - this.currentConversationId, - ); - const messages = conversation?.messages || []; - - await this.agentManager.saveCheckpoint( - messages, - this.currentConversationId, - ); - } catch (error) { - console.warn('[SessionMessageHandler] Failed to auto-save:', error); - } - } - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workingDir = workspaceFolder?.uri.fsPath || process.cwd(); @@ -489,19 +479,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to create a new session.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -525,19 +506,11 @@ export class SessionMessageHandler extends BaseMessageHandler { // If not connected yet, offer to login or view offline if (!this.agentManager.isConnected) { - const selection = await vscode.window.showWarningMessage( + const choice = await this.promptLoginOrOffline( 'You are not logged in. Login now to fully restore this session, or view it offline.', - 'Login Now', - 'View Offline', ); - if (selection === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else if (selection === 'View Offline') { + if (choice === 'offline') { // Show messages from local cache only const messages = await this.agentManager.getSessionMessages(sessionId); @@ -550,33 +523,12 @@ export class SessionMessageHandler extends BaseMessageHandler { 'Showing cached session content. Login to interact with the AI.', ); return; - } else { + } else if (choice !== 'login') { // User dismissed; do nothing return; } } - // Save current session before switching - if ( - this.currentConversationId && - this.currentConversationId !== sessionId && - this.agentManager.isConnected - ) { - try { - const conversation = await this.conversationStore.getConversation( - this.currentConversationId, - ); - const messages = conversation?.messages || []; - - await this.agentManager.saveCheckpoint( - messages, - this.currentConversationId, - ); - } catch (error) { - console.warn('[SessionMessageHandler] Failed to auto-save:', error); - } - } - // Get session details (includes cwd and filePath when using ACP) let sessionDetails: Record | null = null; try { @@ -637,19 +589,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to switch sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -706,19 +649,10 @@ export class SessionMessageHandler extends BaseMessageHandler { createErrorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to switch sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -755,19 +689,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to switch sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -819,19 +744,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to view sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -855,11 +771,6 @@ export class SessionMessageHandler extends BaseMessageHandler { throw new Error('No active conversation to save'); } - const conversation = await this.conversationStore.getConversation( - this.currentConversationId, - ); - const messages = conversation?.messages || []; - // Try ACP save first try { const response = await this.agentManager.saveSessionViaAcp( @@ -883,19 +794,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to save sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -903,17 +805,6 @@ export class SessionMessageHandler extends BaseMessageHandler { }); return; } - - // Fallback to direct save - const response = await this.agentManager.saveSessionDirect( - messages, - tag, - ); - - this.sendToWebView({ - type: 'saveSessionResponse', - data: response, - }); } await this.handleGetQwenSessions(); @@ -931,19 +822,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to save sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -996,19 +878,11 @@ export class SessionMessageHandler extends BaseMessageHandler { try { // If not connected, offer to login or view offline if (!this.agentManager.isConnected) { - const selection = await vscode.window.showWarningMessage( + const choice = await this.promptLoginOrOffline( 'You are not logged in. Login now to fully restore this session, or view it offline.', - 'Login Now', - 'View Offline', ); - if (selection === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } else if (selection === 'View Offline') { + if (choice === 'offline') { const messages = await this.agentManager.getSessionMessages(sessionId); this.currentConversationId = sessionId; @@ -1020,7 +894,7 @@ export class SessionMessageHandler extends BaseMessageHandler { 'Showing cached session content. Login to interact with the AI.', ); return; - } else { + } else if (choice !== 'login') { return; } } @@ -1054,19 +928,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to resume sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -1074,20 +939,6 @@ export class SessionMessageHandler extends BaseMessageHandler { }); return; } - - // Fallback to direct load - const messages = await this.agentManager.loadSessionDirect(sessionId); - - if (messages) { - this.currentConversationId = sessionId; - - this.sendToWebView({ - type: 'qwenSessionSwitched', - data: { sessionId, messages }, - }); - } else { - throw new Error('Failed to load session'); - } } await this.handleGetQwenSessions(); @@ -1105,19 +956,10 @@ export class SessionMessageHandler extends BaseMessageHandler { errorMsg.includes('No active ACP session') ) { // Show a more user-friendly error message for expired sessions - const result = await vscode.window.showWarningMessage( + await this.promptLogin( 'Your login session has expired or is invalid. Please login again to resume sessions.', - 'Login Now', ); - if (result === 'Login Now') { - if (this.loginHandler) { - await this.loginHandler(); - } else { - await vscode.commands.executeCommand('qwen-code.login'); - } - } - // Send a specific error to the webview for better UI handling this.sendToWebView({ type: 'sessionExpired', @@ -1131,4 +973,23 @@ export class SessionMessageHandler extends BaseMessageHandler { } } } + + /** + * Set approval mode via agent (ACP session/set_mode) + */ + private async handleSetApprovalMode(data?: { + modeId?: ApprovalModeValue; + }): Promise { + try { + const modeId = data?.modeId || 'default'; + await this.agentManager.setApprovalModeFromUi(modeId); + // No explicit response needed; WebView listens for modeChanged + } catch (error) { + console.error('[SessionMessageHandler] Failed to set mode:', error); + this.sendToWebView({ + type: 'error', + data: { message: `Failed to set mode: ${error}` }, + }); + } + } } diff --git a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts b/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts deleted file mode 100644 index 7ea8e732..00000000 --- a/packages/vscode-ide-companion/src/webview/handlers/SettingsMessageHandler.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as vscode from 'vscode'; -import { BaseMessageHandler } from './BaseMessageHandler.js'; -import type { ApprovalModeValue } from '../../types/acpTypes.js'; - -/** - * Settings message handler - * Handles all settings-related messages - */ -export class SettingsMessageHandler extends BaseMessageHandler { - canHandle(messageType: string): boolean { - return ['openSettings', 'recheckCli', 'setApprovalMode'].includes( - messageType, - ); - } - - async handle(message: { type: string; data?: unknown }): Promise { - switch (message.type) { - case 'openSettings': - await this.handleOpenSettings(); - break; - - case 'recheckCli': - await this.handleRecheckCli(); - break; - - case 'setApprovalMode': - await this.handleSetApprovalMode( - message.data as { - modeId?: ApprovalModeValue; - }, - ); - break; - - default: - console.warn( - '[SettingsMessageHandler] Unknown message type:', - message.type, - ); - break; - } - } - - /** - * Open settings page - */ - private async handleOpenSettings(): Promise { - try { - // Open settings in a side panel - await vscode.commands.executeCommand('workbench.action.openSettings', { - query: 'qwenCode', - }); - } catch (error) { - console.error('[SettingsMessageHandler] Failed to open settings:', error); - vscode.window.showErrorMessage(`Failed to open settings: ${error}`); - } - } - - /** - * Recheck CLI - */ - private async handleRecheckCli(): Promise { - try { - await vscode.commands.executeCommand('qwenCode.recheckCli'); - this.sendToWebView({ - type: 'cliRechecked', - data: { success: true }, - }); - } catch (error) { - console.error('[SettingsMessageHandler] Failed to recheck CLI:', error); - this.sendToWebView({ - type: 'error', - data: { message: `Failed to recheck CLI: ${error}` }, - }); - } - } - - /** - * Set approval mode via agent (ACP session/set_mode) - */ - private async handleSetApprovalMode(data?: { - modeId?: ApprovalModeValue; - }): Promise { - try { - const modeId = data?.modeId || 'default'; - await this.agentManager.setApprovalModeFromUi(modeId); - // No explicit response needed; WebView listens for modeChanged - } catch (error) { - console.error('[SettingsMessageHandler] Failed to set mode:', error); - this.sendToWebView({ - type: 'error', - data: { message: `Failed to set mode: ${error}` }, - }); - } - } -} diff --git a/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts b/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts index eca8437d..8bccc658 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/file/useFileContext.ts @@ -34,6 +34,9 @@ export const useFileContext = (vscode: VSCodeAPI) => { // Whether workspace files have been requested const hasRequestedFilesRef = useRef(false); + // Last non-empty query to decide when to refetch full list + const lastQueryRef = useRef(undefined); + // Search debounce timer const searchTimerRef = useRef(null); @@ -42,12 +45,10 @@ export const useFileContext = (vscode: VSCodeAPI) => { */ const requestWorkspaceFiles = useCallback( (query?: string) => { - if (!hasRequestedFilesRef.current && !query) { - hasRequestedFilesRef.current = true; - } + const normalizedQuery = query?.trim(); // If there's a query, clear previous timer and set up debounce - if (query && query.length >= 1) { + if (normalizedQuery && normalizedQuery.length >= 1) { if (searchTimerRef.current) { clearTimeout(searchTimerRef.current); } @@ -55,14 +56,23 @@ export const useFileContext = (vscode: VSCodeAPI) => { searchTimerRef.current = setTimeout(() => { vscode.postMessage({ type: 'getWorkspaceFiles', - data: { query }, + data: { query: normalizedQuery }, }); }, 300); + lastQueryRef.current = normalizedQuery; } else { - vscode.postMessage({ - type: 'getWorkspaceFiles', - data: query ? { query } : {}, - }); + // For empty query, request once initially and whenever we are returning from a search + const shouldRequestFullList = + !hasRequestedFilesRef.current || lastQueryRef.current !== undefined; + + if (shouldRequestFullList) { + lastQueryRef.current = undefined; + hasRequestedFilesRef.current = true; + vscode.postMessage({ + type: 'getWorkspaceFiles', + data: {}, + }); + } } }, [vscode], diff --git a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts index 8f6848c1..b18843ef 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useCompletionTrigger.ts @@ -131,12 +131,55 @@ export function useCompletionTrigger( [getCompletionItems, LOADING_ITEM, TIMEOUT_ITEM], ); + // Helper function to compare completion items arrays + const areItemsEqual = ( + items1: CompletionItem[], + items2: CompletionItem[], + ): boolean => { + if (items1.length !== items2.length) { + return false; + } + + // Compare each item by stable fields (ignore non-deterministic props like icons) + for (let i = 0; i < items1.length; i++) { + const a = items1[i]; + const b = items2[i]; + if (a.id !== b.id) { + return false; + } + if (a.label !== b.label) { + return false; + } + if ((a.description ?? '') !== (b.description ?? '')) { + return false; + } + if (a.type !== b.type) { + return false; + } + if ((a.value ?? '') !== (b.value ?? '')) { + return false; + } + if ((a.path ?? '') !== (b.path ?? '')) { + return false; + } + } + + return true; + }; + const refreshCompletion = useCallback(async () => { if (!state.isOpen || !state.triggerChar) { return; } const items = await getCompletionItems(state.triggerChar, state.query); - setState((prev) => ({ ...prev, items })); + + // Only update state if items have actually changed + setState((prev) => { + if (areItemsEqual(prev.items, items)) { + return prev; + } + return { ...prev, items }; + }); }, [state.isOpen, state.triggerChar, state.query, getCompletionItems]); useEffect(() => { diff --git a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts index 9f67bcc8..a91594c0 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.ts @@ -14,6 +14,7 @@ interface UseMessageSubmitProps { setInputText: (text: string) => void; inputFieldRef: React.RefObject; isStreaming: boolean; + isWaitingForResponse: boolean; // When true, do NOT auto-attach the active editor file/selection to context skipAutoActiveContext?: boolean; @@ -40,6 +41,7 @@ export const useMessageSubmit = ({ setInputText, inputFieldRef, isStreaming, + isWaitingForResponse, skipAutoActiveContext = false, fileContext, messageHandling, @@ -48,7 +50,7 @@ export const useMessageSubmit = ({ (e: React.FormEvent) => { e.preventDefault(); - if (!inputText.trim() || isStreaming) { + if (!inputText.trim() || isStreaming || isWaitingForResponse) { return; } @@ -56,7 +58,10 @@ export const useMessageSubmit = ({ if (inputText.trim() === '/login') { setInputText(''); if (inputFieldRef.current) { - inputFieldRef.current.textContent = ''; + // Use a zero-width space to maintain the height of the contentEditable element + inputFieldRef.current.textContent = '\u200B'; + // Set the data-empty attribute to show the placeholder + inputFieldRef.current.setAttribute('data-empty', 'true'); } vscode.postMessage({ type: 'login', @@ -142,7 +147,10 @@ export const useMessageSubmit = ({ setInputText(''); if (inputFieldRef.current) { - inputFieldRef.current.textContent = ''; + // Use a zero-width space to maintain the height of the contentEditable element + inputFieldRef.current.textContent = '\u200B'; + // Set the data-empty attribute to show the placeholder + inputFieldRef.current.setAttribute('data-empty', 'true'); } fileContext.clearFileReferences(); }, @@ -154,6 +162,7 @@ export const useMessageSubmit = ({ vscode, fileContext, skipAutoActiveContext, + isWaitingForResponse, messageHandling, ], ); diff --git a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts index cd312361..c8d507f2 100644 --- a/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts +++ b/packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.ts @@ -12,7 +12,7 @@ import type { ToolCall as PermissionToolCall, } from '../components/PermissionDrawer/PermissionRequest.js'; import type { ToolCallUpdate } from '../../types/chatTypes.js'; -import type { ApprovalModeValue } from '../../types/acpTypes.js'; +import type { ApprovalModeValue } from '../../types/approvalModeValueTypes.js'; import type { PlanEntry } from '../../types/chatTypes.js'; interface UseWebViewMessagesProps { @@ -109,6 +109,8 @@ interface UseWebViewMessagesProps { setInputText: (text: string) => void; // Edit mode setter (maps ACP modes to UI modes) setEditMode?: (mode: ApprovalModeValue) => void; + // Authentication state setter + setIsAuthenticated?: (authenticated: boolean | null) => void; } /** @@ -126,6 +128,7 @@ export const useWebViewMessages = ({ inputFieldRef, setInputText, setEditMode, + setIsAuthenticated, }: UseWebViewMessagesProps) => { // VS Code API for posting messages back to the extension host const vscode = useVSCode(); @@ -141,6 +144,7 @@ export const useWebViewMessages = ({ clearToolCalls, setPlanEntries, handlePermissionRequest, + setIsAuthenticated, }); // Track last "Updated Plan" snapshot toolcall to support merge/dedupe @@ -185,6 +189,7 @@ export const useWebViewMessages = ({ clearToolCalls, setPlanEntries, handlePermissionRequest, + setIsAuthenticated, }; }); @@ -216,6 +221,7 @@ export const useWebViewMessages = ({ } break; } + case 'loginSuccess': { // Clear loading state and show a short assistant notice handlers.messageHandling.clearWaitingForResponse(); @@ -224,43 +230,35 @@ export const useWebViewMessages = ({ content: 'Successfully logged in. You can continue chatting.', timestamp: Date.now(), }); + // Set authentication state to true + handlers.setIsAuthenticated?.(true); break; } - // case 'cliNotInstalled': { - // // Show CLI not installed message - // const errorMsg = - // (message?.data?.error as string) || - // 'Qwen Code CLI is not installed. Please install it to enable full functionality.'; + case 'agentConnected': { + // Agent connected successfully; clear any pending spinner + handlers.messageHandling.clearWaitingForResponse(); + // Set authentication state to true + handlers.setIsAuthenticated?.(true); + break; + } - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Qwen CLI is not installed. Please install it to enable full functionality.\n\nError: ${errorMsg}\n\nInstallation instructions:\n1. Install via npm:\n npm install -g @qwen-code/qwen-code@latest\n\n2. After installation, reload VS Code or restart the extension.`, - // timestamp: Date.now(), - // }); - // break; - // } + case 'agentConnectionError': { + // Agent connection failed; surface the error and unblock the UI + handlers.messageHandling.clearWaitingForResponse(); + const errorMsg = + (message?.data?.message as string) || + 'Failed to connect to Qwen agent.'; - // case 'agentConnected': { - // // Agent connected successfully - // handlers.messageHandling.clearWaitingForResponse(); - // break; - // } - - // case 'agentConnectionError': { - // // Agent connection failed - // handlers.messageHandling.clearWaitingForResponse(); - // const errorMsg = - // (message?.data?.message as string) || - // 'Failed to connect to Qwen agent.'; - - // handlers.messageHandling.addMessage({ - // role: 'assistant', - // content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, - // timestamp: Date.now(), - // }); - // break; - // } + handlers.messageHandling.addMessage({ + role: 'assistant', + content: `Failed to connect to Qwen agent: ${errorMsg}\nYou can still use the chat UI, but messages won't be sent to AI.`, + timestamp: Date.now(), + }); + // Set authentication state to false + handlers.setIsAuthenticated?.(false); + break; + } case 'loginError': { // Clear loading state and show error notice @@ -273,6 +271,20 @@ export const useWebViewMessages = ({ content: errorMsg, timestamp: Date.now(), }); + // Set authentication state to false + handlers.setIsAuthenticated?.(false); + break; + } + + case 'authState': { + const state = ( + message?.data as { authenticated?: boolean | null } | undefined + )?.authenticated; + if (typeof state === 'boolean') { + handlers.setIsAuthenticated?.(state); + } else { + handlers.setIsAuthenticated?.(null); + } break; } @@ -338,30 +350,42 @@ export const useWebViewMessages = ({ } case 'streamEnd': { - // Always end local streaming state and collapse any thoughts + // Always end local streaming state and clear thinking state handlers.messageHandling.endStreaming(); handlers.messageHandling.clearThinking(); - // If the stream ended due to explicit user cancel, proactively - // clear the waiting indicator and reset any tracked exec calls. - // This avoids the UI being stuck with the Stop button visible - // after rejecting a permission request. + // If stream ended due to explicit user cancellation, proactively clear + // waiting indicator and reset tracked execution calls. + // This avoids UI getting stuck with Stop button visible after + // rejecting a permission request. try { const reason = ( (message.data as { reason?: string } | undefined)?.reason || '' ).toLowerCase(); - if (reason === 'user_cancelled') { + + /** + * Handle different types of stream end reasons: + * - 'user_cancelled': User explicitly cancelled operation + * - 'cancelled': General cancellation + * For these cases, immediately clear all active states + */ + if (reason === 'user_cancelled' || reason === 'cancelled') { + // Clear active execution tool call tracking, reset state activeExecToolCallsRef.current.clear(); + // Clear waiting response state to ensure UI returns to normal handlers.messageHandling.clearWaitingForResponse(); break; } } catch (_error) { - // best-effort + // Best-effort handling, errors don't affect main flow } - // Otherwise, clear the generic waiting indicator only if there are - // no active long-running tool calls. If there are still active - // execute/bash/command calls, keep the hint visible. + /** + * For other types of stream end (non-user cancellation): + * Only clear generic waiting indicator when there are no active + * long-running tool calls. If there are still active execute/bash/command + * calls, keep the hint visible. + */ if (activeExecToolCallsRef.current.size === 0) { handlers.messageHandling.clearWaitingForResponse(); } @@ -562,15 +586,21 @@ export const useWebViewMessages = ({ // While long-running tools (e.g., execute/bash/command) are in progress, // surface a lightweight loading indicator and expose the Stop button. try { + const id = (toolCallData.toolCallId || '').toString(); const kind = (toolCallData.kind || '').toString().toLowerCase(); - const isExec = + const isExecKind = kind === 'execute' || kind === 'bash' || kind === 'command'; + // CLI sometimes omits kind in tool_call_update payloads; fall back to + // whether we've already tracked this ID as an exec tool. + const wasTrackedExec = activeExecToolCallsRef.current.has(id); + const isExec = isExecKind || wasTrackedExec; - if (isExec) { - const id = (toolCallData.toolCallId || '').toString(); + if (!isExec || !id) { + break; + } - // Maintain the active set by status - if (status === 'pending' || status === 'in_progress') { + if (status === 'pending' || status === 'in_progress') { + if (isExecKind) { activeExecToolCallsRef.current.add(id); // Build a helpful hint from rawInput @@ -584,14 +614,14 @@ export const useWebViewMessages = ({ } const hint = cmd ? `Running: ${cmd}` : 'Running command...'; handlers.messageHandling.setWaitingForResponse(hint); - } else if (status === 'completed' || status === 'failed') { - activeExecToolCallsRef.current.delete(id); } + } else if (status === 'completed' || status === 'failed') { + activeExecToolCallsRef.current.delete(id); + } - // If no active exec tool remains, clear the waiting message. - if (activeExecToolCallsRef.current.size === 0) { - handlers.messageHandling.clearWaitingForResponse(); - } + // If no active exec tool remains, clear the waiting message. + if (activeExecToolCallsRef.current.size === 0) { + handlers.messageHandling.clearWaitingForResponse(); } } catch (_error) { // Best-effort UI hint; ignore errors diff --git a/packages/vscode-ide-companion/src/webview/styles/styles.css b/packages/vscode-ide-companion/src/webview/styles/styles.css index 4c3db053..956912cb 100644 --- a/packages/vscode-ide-companion/src/webview/styles/styles.css +++ b/packages/vscode-ide-companion/src/webview/styles/styles.css @@ -5,7 +5,6 @@ */ /* Import component styles */ -@import '../components/messages/Assistant/AssistantMessage.css'; @import './timeline.css'; @import '../components/messages/MarkdownRenderer/MarkdownRenderer.css'; diff --git a/packages/vscode-ide-companion/src/webview/styles/tailwind.css b/packages/vscode-ide-companion/src/webview/styles/tailwind.css index 4c4b5e08..46d803d5 100644 --- a/packages/vscode-ide-companion/src/webview/styles/tailwind.css +++ b/packages/vscode-ide-companion/src/webview/styles/tailwind.css @@ -51,8 +51,7 @@ .composer-form:focus-within { /* match existing highlight behavior */ border-color: var(--app-input-highlight); - box-shadow: 0 1px 2px - color-mix(in srgb, var(--app-input-highlight), transparent 80%); + box-shadow: 0 1px 2px color-mix(in srgb, var(--app-input-highlight), transparent 80%); } /* Composer: input editable area */ @@ -67,7 +66,7 @@ The data attribute is needed because some browsers insert a
in contentEditable, which breaks :empty matching. */ .composer-input:empty:before, - .composer-input[data-empty='true']::before { + .composer-input[data-empty="true"]::before { content: attr(data-placeholder); color: var(--app-input-placeholder-foreground); pointer-events: none; @@ -81,7 +80,7 @@ outline: none; } .composer-input:disabled, - .composer-input[contenteditable='false'] { + .composer-input[contenteditable="false"] { color: #999; cursor: not-allowed; } @@ -111,7 +110,7 @@ } .btn-text-compact > svg { height: 1em; - width: 1em; + width: 1em; flex-shrink: 0; } .btn-text-compact > span { diff --git a/packages/vscode-ide-companion/src/webview/styles/timeline.css b/packages/vscode-ide-companion/src/webview/styles/timeline.css index 25d5cc85..033e82d2 100644 --- a/packages/vscode-ide-companion/src/webview/styles/timeline.css +++ b/packages/vscode-ide-companion/src/webview/styles/timeline.css @@ -88,6 +88,22 @@ z-index: 0; } +/* Single-item AI sequence (both a start and an end): hide the connector entirely */ +.qwen-message.message-item:not(.user-message-container):is( + :first-child, + .user-message-container + + .qwen-message.message-item:not(.user-message-container), + .chat-messages + > :not(.qwen-message.message-item) + + .qwen-message.message-item:not(.user-message-container) + ):is( + :has(+ .user-message-container), + :has(+ :not(.qwen-message.message-item)), + :last-child + )::after { + display: none; +} + /* Handle the start of each AI message sequence - includes the first AI message in the entire message list and new AI messages interrupted by user messages */ .qwen-message.message-item:not(.user-message-container):first-child::after, .user-message-container + .qwen-message.message-item:not(.user-message-container)::after, @@ -123,4 +139,4 @@ position: relative; padding-top: 8px; padding-bottom: 8px; -} \ No newline at end of file +} diff --git a/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts b/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts index 31326cc6..e11f4bce 100644 --- a/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts +++ b/packages/vscode-ide-companion/src/webview/utils/sessionGrouping.ts @@ -62,3 +62,38 @@ export const groupSessionsByDate = ( .filter(([, sessions]) => sessions.length > 0) .map(([label, sessions]) => ({ label, sessions })); }; + +/** + * Time ago formatter + * + * @param timestamp - ISO timestamp string + * @returns Formatted time string + */ +export const getTimeAgo = (timestamp: string): string => { + if (!timestamp) { + return ''; + } + const now = new Date().getTime(); + const then = new Date(timestamp).getTime(); + const diffMs = now - then; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) { + return 'now'; + } + if (diffMins < 60) { + return `${diffMins}m`; + } + if (diffHours < 24) { + return `${diffHours}h`; + } + if (diffDays === 1) { + return 'Yesterday'; + } + if (diffDays < 7) { + return `${diffDays}d`; + } + return new Date(timestamp).toLocaleDateString(); +}; diff --git a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts b/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts deleted file mode 100644 index 0231f383..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/simpleDiff.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - * - * Minimal line-diff utility for webview previews. - * - * This is a lightweight LCS-based algorithm to compute add/remove operations - * between two texts. It intentionally avoids heavy dependencies and is - * sufficient for rendering a compact preview inside the chat. - */ - -export type DiffOp = - | { type: 'add'; line: string; newIndex: number } - | { type: 'remove'; line: string; oldIndex: number }; - -/** - * Compute a minimal line-diff (added/removed only). - * - Equal lines are omitted from output by design (we only preview changes). - * - Order of operations follows the new text progression so the preview feels natural. - */ -export function computeLineDiff( - oldText: string | null | undefined, - newText: string | undefined, -): DiffOp[] { - const a = (oldText || '').split('\n'); - const b = (newText || '').split('\n'); - - const n = a.length; - const m = b.length; - - // Build LCS DP table - const dp: number[][] = Array.from({ length: n + 1 }, () => - new Array(m + 1).fill(0), - ); - for (let i = n - 1; i >= 0; i--) { - for (let j = m - 1; j >= 0; j--) { - if (a[i] === b[j]) { - dp[i][j] = dp[i + 1][j + 1] + 1; - } else { - dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]); - } - } - } - - // Walk to produce operations - const ops: DiffOp[] = []; - let i = 0; - let j = 0; - while (i < n && j < m) { - if (a[i] === b[j]) { - i++; - j++; - } else if (dp[i + 1][j] >= dp[i][j + 1]) { - // remove a[i] - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } else { - // add b[j] - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - } - - // Remaining tails - while (i < n) { - ops.push({ type: 'remove', line: a[i], oldIndex: i }); - i++; - } - while (j < m) { - ops.push({ type: 'add', line: b[j], newIndex: j }); - j++; - } - - return ops; -} - -/** - * Truncate a long list of operations for preview purposes. - * Keeps first `head` and last `tail` operations, inserting a gap marker. - */ -export function truncateOps( - ops: T[], - head = 120, - tail = 80, -): { items: T[]; truncated: boolean; omitted: number } { - if (ops.length <= head + tail) { - return { items: ops, truncated: false, omitted: 0 }; - } - const items = [...ops.slice(0, head), ...ops.slice(-tail)]; - return { items, truncated: true, omitted: ops.length - head - tail }; -} diff --git a/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts b/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts deleted file mode 100644 index b1610597..00000000 --- a/packages/vscode-ide-companion/src/webview/utils/timeUtils.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @license - * Copyright 2025 Qwen Team - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * Time ago formatter - * - * @param timestamp - ISO timestamp string - * @returns Formatted time string - */ -export const getTimeAgo = (timestamp: string): string => { - if (!timestamp) { - return ''; - } - const now = new Date().getTime(); - const then = new Date(timestamp).getTime(); - const diffMs = now - then; - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) { - return 'now'; - } - if (diffMins < 60) { - return `${diffMins}m`; - } - if (diffHours < 24) { - return `${diffHours}h`; - } - if (diffDays === 1) { - return 'Yesterday'; - } - if (diffDays < 7) { - return `${diffDays}d`; - } - return new Date(timestamp).toLocaleDateString(); -};