diff --git a/docs/users/configuration/auth.md b/docs/users/configuration/auth.md index 82bc66b26..ca62f2918 100644 --- a/docs/users/configuration/auth.md +++ b/docs/users/configuration/auth.md @@ -5,11 +5,13 @@ Qwen Code supports two authentication methods. Pick the one that matches how you - **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). +![](https://img.alicdn.com/imgextra/i2/O1CN01IxI1bt1sNO543AVTT_!!6000000005754-0-tps-1958-822.jpg) + ## Option 1: Qwen OAuth (recommended & free) 👍 -Use this if you want the simplest setup and you’re using Qwen models. +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. +- **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**. @@ -24,15 +26,54 @@ qwen 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) +### Recommended: Coding Plan (subscription-based) 🚀 -When you choose the OpenAI-compatible option in the CLI, it will prompt you for: +Use this if you want predictable costs with higher usage quotas for the qwen3-coder-plus model. -- **API key** -- **Base URL** (default: `https://api.openai.com/v1`) -- **Model** (default: `gpt-4o`) +> [!IMPORTANT] +> +> Coding Plan is only available for users in China mainland (Beijing region). -> **Note:** the CLI may display the key in plain text for verification. Make sure your terminal is not being recorded or shared. +- **How it works**: subscribe to the Coding Plan with a fixed monthly fee, then configure Qwen Code to use the dedicated endpoint and your subscription API key. +- **Requirements**: an active Coding Plan subscription from [Alibaba Cloud Bailian](https://bailian.console.aliyun.com/cn-beijing/?tab=globalset#/efm/coding_plan). +- **Benefits**: higher usage quotas, predictable monthly costs, access to latest qwen3-coder-plus model. +- **Cost & quota**: varies by plan (see table below). + +#### Coding Plan Pricing & Quotas + +| Feature | Lite Basic Plan | Pro Advanced Plan | +| :------------------ | :-------------------- | :-------------------- | +| **Price** | ¥40/month | ¥200/month | +| **5-Hour Limit** | Up to 1,200 requests | Up to 6,000 requests | +| **Weekly Limit** | Up to 9,000 requests | Up to 45,000 requests | +| **Monthly Limit** | Up to 18,000 requests | Up to 90,000 requests | +| **Supported Model** | qwen3-coder-plus | qwen3-coder-plus | + +#### Quick Setup for Coding Plan + +When you select the OpenAI-compatible option in the CLI, enter these values: + +- **API key**: `sk-sp-xxxxx` +- **Base URL**: `https://coding.dashscope.aliyuncs.com/v1` +- **Model**: `qwen3-coder-plus` + +> **Note**: Coding Plan API keys have the format `sk-sp-xxxxx`, which is different from standard Alibaba Cloud API keys. + +#### Configure via Environment Variables + +Set these environment variables to use Coding Plan: + +```bash +export OPENAI_API_KEY="your-coding-plan-api-key" # Format: sk-sp-xxxxx +export OPENAI_BASE_URL="https://coding.dashscope.aliyuncs.com/v1" +export OPENAI_MODEL="qwen3-coder-plus" +``` + +For more details about Coding Plan, including subscription options and troubleshooting, see the [full Coding Plan documentation](https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=3005961). + +### Other OpenAI-compatible Providers + +If you are using other providers (OpenAI, Azure, local LLMs, etc.), use the following configuration methods. ### Configure via command-line arguments diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md index 03a029a6a..a7b337b44 100644 --- a/docs/users/configuration/settings.md +++ b/docs/users/configuration/settings.md @@ -271,6 +271,7 @@ If you are experiencing performance issues with file searching (e.g., with `@` c | `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.experimental.skills` | boolean | Enable experimental Agent Skills feature | `false` | | #### mcp diff --git a/docs/users/features/skills.md b/docs/users/features/skills.md index 0387ff389..0e55644ab 100644 --- a/docs/users/features/skills.md +++ b/docs/users/features/skills.md @@ -11,12 +11,29 @@ This guide shows you how to create, use, and manage Agent Skills in **Qwen Code* ## Prerequisites - Qwen Code (recent version) -- Run with the experimental flag enabled: + +## How to enable + +### Via CLI flag ```bash qwen --experimental-skills ``` +### Via settings.json + +Add to your `~/.qwen/settings.json` or project's `.qwen/settings.json`: + +```json +{ + "tools": { + "experimental": { + "skills": true + } + } +} +``` + - Basic familiarity with Qwen Code ([Quickstart](../quickstart.md)) ## What are Agent Skills? diff --git a/package-lock.json b/package-lock.json index f4eaa1e13..a5459d69d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@qwen-code/qwen-code", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@qwen-code/qwen-code", - "version": "0.7.0", + "version": "0.7.1", "workspaces": [ "packages/*" ], @@ -17310,7 +17310,7 @@ }, "packages/cli": { "name": "@qwen-code/qwen-code", - "version": "0.7.0", + "version": "0.7.1", "dependencies": { "@google/genai": "1.30.0", "@iarna/toml": "^2.2.5", @@ -17947,7 +17947,7 @@ }, "packages/core": { "name": "@qwen-code/qwen-code-core", - "version": "0.7.0", + "version": "0.7.1", "hasInstallScript": true, "dependencies": { "@anthropic-ai/sdk": "^0.36.1", @@ -21408,7 +21408,7 @@ }, "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", - "version": "0.7.0", + "version": "0.7.1", "dev": true, "license": "Apache-2.0", "devDependencies": { @@ -21420,7 +21420,7 @@ }, "packages/vscode-ide-companion": { "name": "qwen-code-vscode-ide-companion", - "version": "0.7.0", + "version": "0.7.1", "license": "LICENSE", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", diff --git a/package.json b/package.json index 81c788628..eb9c9a75f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code", - "version": "0.7.0", + "version": "0.7.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.7.0" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1" }, "scripts": { "start": "cross-env node scripts/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7c0b14bd6..f7b26a605 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code", - "version": "0.7.0", + "version": "0.7.1", "description": "Qwen Code", "repository": { "type": "git", @@ -33,7 +33,7 @@ "dist" ], "config": { - "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.0" + "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.7.1" }, "dependencies": { "@google/genai": "1.30.0", diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 42ed430d5..c51b4d954 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -328,7 +328,7 @@ export async function parseArguments(settings: Settings): Promise { .option('experimental-skills', { type: 'boolean', description: 'Enable experimental Skills feature', - default: false, + default: settings.tools?.experimental?.skills ?? false, }) .option('channel', { type: 'string', @@ -864,11 +864,10 @@ export async function loadCliConfig( } }; - if ( - !interactive && - !argv.experimentalAcp && - inputFormat !== InputFormat.STREAM_JSON - ) { + // ACP mode check: must include both --acp (current) and --experimental-acp (deprecated). + // Without this check, edit, write_file, run_shell_command would be excluded in ACP mode. + const isAcpMode = argv.acp || argv.experimentalAcp; + if (!interactive && !isAcpMode && inputFormat !== InputFormat.STREAM_JSON) { switch (approvalMode) { case ApprovalMode.PLAN: case ApprovalMode.DEFAULT: diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index d51585826..90413e5a5 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -905,6 +905,27 @@ const SETTINGS_SCHEMA = { description: 'The number of lines to keep when truncating tool output.', showInDialog: false, }, + experimental: { + type: 'object', + label: 'Experimental', + category: 'Tools', + requiresRestart: true, + default: {}, + description: 'Experimental tool features.', + showInDialog: false, + properties: { + skills: { + type: 'boolean', + label: 'Skills', + category: 'Tools', + requiresRestart: true, + default: false, + description: + 'Enable experimental Agent Skills feature. When enabled, Qwen Code can use Skills from .qwen/skills/ and ~/.qwen/skills/.', + showInDialog: true, + }, + }, + }, }, }, diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 9920393a2..61a23677b 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -908,11 +908,11 @@ export default { 'Session Stats': '会话统计', 'Model Usage': '模型使用情况', Reqs: '请求数', - 'Input Tokens': '输入令牌', - 'Output Tokens': '输出令牌', + 'Input Tokens': '输入 token 数', + 'Output Tokens': '输出 token 数', 'Savings Highlight:': '节省亮点:', 'of input tokens were served from the cache, reducing costs.': - '的输入令牌来自缓存,降低了成本', + '从缓存载入 token ,降低了成本', 'Tip: For a full token breakdown, run `/stats model`.': '提示:要查看完整的令牌明细,请运行 `/stats model`', 'Model Stats For Nerds': '模型统计(技术细节)', diff --git a/packages/cli/src/utils/commentJson.test.ts b/packages/cli/src/utils/commentJson.test.ts index 4957b7497..fcf2501cd 100644 --- a/packages/cli/src/utils/commentJson.test.ts +++ b/packages/cli/src/utils/commentJson.test.ts @@ -8,7 +8,10 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; -import { updateSettingsFilePreservingFormat } from './commentJson.js'; +import { + updateSettingsFilePreservingFormat, + applyUpdates, +} from './commentJson.js'; describe('commentJson', () => { let tempDir: string; @@ -180,3 +183,18 @@ describe('commentJson', () => { }); }); }); + +describe('applyUpdates', () => { + it('should apply updates correctly', () => { + const original = { a: 1, b: { c: 2 } }; + const updates = { b: { c: 3 } }; + const result = applyUpdates(original, updates); + expect(result).toEqual({ a: 1, b: { c: 3 } }); + }); + it('should apply updates correctly when empty', () => { + const original = { a: 1, b: { c: 2 } }; + const updates = { b: {} }; + const result = applyUpdates(original, updates); + expect(result).toEqual({ a: 1, b: {} }); + }); +}); diff --git a/packages/cli/src/utils/commentJson.ts b/packages/cli/src/utils/commentJson.ts index 9ea4d3f80..bf325d9af 100644 --- a/packages/cli/src/utils/commentJson.ts +++ b/packages/cli/src/utils/commentJson.ts @@ -38,7 +38,7 @@ export function updateSettingsFilePreservingFormat( fs.writeFileSync(filePath, updatedContent, 'utf-8'); } -function applyUpdates( +export function applyUpdates( current: Record, updates: Record, ): Record { @@ -50,6 +50,7 @@ function applyUpdates( typeof value === 'object' && value !== null && !Array.isArray(value) && + Object.keys(value).length > 0 && typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key]) diff --git a/packages/core/package.json b/packages/core/package.json index f163b81c0..6c29d02d3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@qwen-code/qwen-code-core", - "version": "0.7.0", + "version": "0.7.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 8e4a06ee7..48dfe594a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -403,7 +403,7 @@ export class Config { private toolRegistry!: ToolRegistry; private promptRegistry!: PromptRegistry; private subagentManager!: SubagentManager; - private skillManager!: SkillManager; + private skillManager: SkillManager | null = null; private fileSystemService: FileSystemService; private contentGeneratorConfig!: ContentGeneratorConfig; private contentGeneratorConfigSources: ContentGeneratorConfigSources = {}; @@ -669,8 +669,10 @@ export class Config { } this.promptRegistry = new PromptRegistry(); this.subagentManager = new SubagentManager(this); - this.skillManager = new SkillManager(this); - await this.skillManager.startWatching(); + if (this.getExperimentalSkills()) { + this.skillManager = new SkillManager(this); + await this.skillManager.startWatching(); + } // Load session subagents if they were provided before initialization if (this.sessionSubagents.length > 0) { @@ -1432,7 +1434,7 @@ export class Config { return this.subagentManager; } - getSkillManager(): SkillManager { + getSkillManager(): SkillManager | null { return this.skillManager; } diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 476776cb6..8849400a5 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -270,28 +270,28 @@ export function createContentGeneratorConfig( } export async function createContentGenerator( - config: ContentGeneratorConfig, - gcConfig: Config, + generatorConfig: ContentGeneratorConfig, + config: Config, isInitialAuth?: boolean, ): Promise { - const validation = validateModelConfig(config, false); + const validation = validateModelConfig(generatorConfig, false); if (!validation.valid) { throw new Error(validation.errors.map((e) => e.message).join('\n')); } - if (config.authType === AuthType.USE_OPENAI) { - // Import OpenAIContentGenerator dynamically to avoid circular dependencies + const authType = generatorConfig.authType; + if (!authType) { + throw new Error('ContentGeneratorConfig must have an authType'); + } + + let baseGenerator: ContentGenerator; + + if (authType === AuthType.USE_OPENAI) { const { createOpenAIContentGenerator } = await import( './openaiContentGenerator/index.js' ); - - // Always use OpenAIContentGenerator, logging is controlled by enableOpenAILogging flag - const generator = createOpenAIContentGenerator(config, gcConfig); - return new LoggingContentGenerator(generator, gcConfig); - } - - if (config.authType === AuthType.QWEN_OAUTH) { - // Import required classes dynamically + baseGenerator = createOpenAIContentGenerator(generatorConfig, config); + } else if (authType === AuthType.QWEN_OAUTH) { const { getQwenOAuthClient: getQwenOauthClient } = await import( '../qwen/qwenOAuth2.js' ); @@ -300,44 +300,38 @@ export async function createContentGenerator( ); try { - // Get the Qwen OAuth client (now includes integrated token management) - // If this is initial auth, require cached credentials to detect missing credentials const qwenClient = await getQwenOauthClient( - gcConfig, + config, isInitialAuth ? { requireCachedCredentials: true } : undefined, ); - - // Create the content generator with dynamic token management - const generator = new QwenContentGenerator(qwenClient, config, gcConfig); - return new LoggingContentGenerator(generator, gcConfig); + baseGenerator = new QwenContentGenerator( + qwenClient, + generatorConfig, + config, + ); } catch (error) { throw new Error( `${error instanceof Error ? error.message : String(error)}`, ); } - } - - if (config.authType === AuthType.USE_ANTHROPIC) { + } else if (authType === AuthType.USE_ANTHROPIC) { const { createAnthropicContentGenerator } = await import( './anthropicContentGenerator/index.js' ); - - const generator = createAnthropicContentGenerator(config, gcConfig); - return new LoggingContentGenerator(generator, gcConfig); - } - - if ( - config.authType === AuthType.USE_GEMINI || - config.authType === AuthType.USE_VERTEX_AI + baseGenerator = createAnthropicContentGenerator(generatorConfig, config); + } else if ( + authType === AuthType.USE_GEMINI || + authType === AuthType.USE_VERTEX_AI ) { const { createGeminiContentGenerator } = await import( './geminiContentGenerator/index.js' ); - const generator = createGeminiContentGenerator(config, gcConfig); - return new LoggingContentGenerator(generator, gcConfig); + baseGenerator = createGeminiContentGenerator(generatorConfig, config); + } else { + throw new Error( + `Error creating contentGenerator: Unsupported authType: ${authType}`, + ); } - throw new Error( - `Error creating contentGenerator: Unsupported authType: ${config.authType}`, - ); + return new LoggingContentGenerator(baseGenerator, config, generatorConfig); } diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts index 0a61e5573..156b75a01 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.test.ts @@ -12,6 +12,7 @@ import type { import { GenerateContentResponse } from '@google/genai'; import type { Config } from '../../config/config.js'; import type { ContentGenerator } from '../contentGenerator.js'; +import { AuthType } from '../contentGenerator.js'; import { LoggingContentGenerator } from './index.js'; import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js'; import { @@ -50,14 +51,17 @@ const convertGeminiResponseToOpenAISpy = vi choices: [], } as OpenAI.Chat.ChatCompletion); -const createConfig = (overrides: Record = {}): Config => - ({ - getContentGeneratorConfig: () => ({ - authType: 'openai', - enableOpenAILogging: false, - ...overrides, - }), - }) as Config; +const createConfig = (overrides: Record = {}): Config => { + const configContent = { + authType: 'openai', + enableOpenAILogging: false, + ...overrides, + }; + return { + getContentGeneratorConfig: () => configContent, + getAuthType: () => configContent.authType as AuthType | undefined, + } as Config; +}; const createWrappedGenerator = ( generateContent: ContentGenerator['generateContent'], @@ -124,13 +128,17 @@ describe('LoggingContentGenerator', () => { ), vi.fn(), ); + const generatorConfig = { + model: 'test-model', + authType: AuthType.USE_OPENAI, + enableOpenAILogging: true, + openAILoggingDir: 'logs', + schemaCompliance: 'openapi_30' as const, + }; const generator = new LoggingContentGenerator( wrapped, - createConfig({ - enableOpenAILogging: true, - openAILoggingDir: 'logs', - schemaCompliance: 'openapi_30', - }), + createConfig(), + generatorConfig, ); const request = { @@ -225,9 +233,15 @@ describe('LoggingContentGenerator', () => { vi.fn().mockRejectedValue(error), vi.fn(), ); + const generatorConfig = { + model: 'test-model', + authType: AuthType.USE_OPENAI, + enableOpenAILogging: true, + }; const generator = new LoggingContentGenerator( wrapped, - createConfig({ enableOpenAILogging: true }), + createConfig(), + generatorConfig, ); const request = { @@ -293,9 +307,15 @@ describe('LoggingContentGenerator', () => { })(), ), ); + const generatorConfig = { + model: 'test-model', + authType: AuthType.USE_OPENAI, + enableOpenAILogging: true, + }; const generator = new LoggingContentGenerator( wrapped, - createConfig({ enableOpenAILogging: true }), + createConfig(), + generatorConfig, ); const request = { @@ -345,9 +365,15 @@ describe('LoggingContentGenerator', () => { })(), ), ); + const generatorConfig = { + model: 'test-model', + authType: AuthType.USE_OPENAI, + enableOpenAILogging: true, + }; const generator = new LoggingContentGenerator( wrapped, - createConfig({ enableOpenAILogging: true }), + createConfig(), + generatorConfig, ); const request = { diff --git a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts index 34e9128a8..88e9e2c87 100644 --- a/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts +++ b/packages/core/src/core/loggingContentGenerator/loggingContentGenerator.ts @@ -31,7 +31,10 @@ import { logApiRequest, logApiResponse, } from '../../telemetry/loggers.js'; -import type { ContentGenerator } from '../contentGenerator.js'; +import type { + ContentGenerator, + ContentGeneratorConfig, +} from '../contentGenerator.js'; import { isStructuredError } from '../../utils/quotaErrorDetection.js'; import { OpenAIContentConverter } from '../openaiContentGenerator/converter.js'; import { OpenAILogger } from '../../utils/openaiLogger.js'; @@ -50,9 +53,11 @@ export class LoggingContentGenerator implements ContentGenerator { constructor( private readonly wrapped: ContentGenerator, private readonly config: Config, + generatorConfig: ContentGeneratorConfig, ) { - const generatorConfig = this.config.getContentGeneratorConfig(); - if (generatorConfig?.enableOpenAILogging) { + // Extract fields needed for initialization from passed config + // (config.getContentGeneratorConfig() may not be available yet during refreshAuth) + if (generatorConfig.enableOpenAILogging) { this.openaiLogger = new OpenAILogger(generatorConfig.openAILoggingDir); this.schemaCompliance = generatorConfig.schemaCompliance; } @@ -89,7 +94,7 @@ export class LoggingContentGenerator implements ContentGenerator { model, durationMs, prompt_id, - this.config.getContentGeneratorConfig()?.authType, + this.config.getAuthType(), usageMetadata, responseText, ), @@ -126,7 +131,7 @@ export class LoggingContentGenerator implements ContentGenerator { errorMessage, durationMs, prompt_id, - this.config.getContentGeneratorConfig()?.authType, + this.config.getAuthType(), errorType, errorStatus, ), diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 076816f86..3e5125a4d 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -112,6 +112,62 @@ You are a helpful assistant with this skill. expect(config.filePath).toBe(validSkillConfig.filePath); }); + it('should parse markdown with CRLF line endings', () => { + const markdownCrlf = `---\r +name: test-skill\r +description: A test skill\r +---\r +\r +You are a helpful assistant with this skill.\r +`; + + const config = manager.parseSkillContent( + markdownCrlf, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + expect(config.body).toBe('You are a helpful assistant with this skill.'); + }); + + it('should parse markdown with UTF-8 BOM', () => { + const markdownWithBom = `\uFEFF--- +name: test-skill +description: A test skill +--- + +You are a helpful assistant with this skill. +`; + + const config = manager.parseSkillContent( + markdownWithBom, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + }); + + it('should parse markdown when body is empty and file ends after frontmatter', () => { + const frontmatterOnly = `--- +name: test-skill +description: A test skill +---`; + + const config = manager.parseSkillContent( + frontmatterOnly, + validSkillConfig.filePath, + 'project', + ); + + expect(config.name).toBe('test-skill'); + expect(config.description).toBe('A test skill'); + expect(config.body).toBe(''); + }); + it('should parse content with allowedTools', () => { const markdownWithTools = `--- name: test-skill diff --git a/packages/core/src/skills/skill-manager.ts b/packages/core/src/skills/skill-manager.ts index a72205150..6509b712d 100644 --- a/packages/core/src/skills/skill-manager.ts +++ b/packages/core/src/skills/skill-manager.ts @@ -235,6 +235,7 @@ export class SkillManager { } this.watchStarted = true; + await this.ensureUserSkillsDir(); await this.refreshCache(); this.updateWatchersFromCache(); } @@ -306,9 +307,11 @@ export class SkillManager { level: SkillLevel, ): SkillConfig { try { + const normalizedContent = normalizeSkillFileContent(content); + // Split frontmatter and content - const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/; - const match = content.match(frontmatterRegex); + const frontmatterRegex = /^---\n([\s\S]*?)\n---(?:\n|$)([\s\S]*)$/; + const match = normalizedContent.match(frontmatterRegex); if (!match) { throw new Error('Invalid format: missing YAML frontmatter'); @@ -486,29 +489,14 @@ export class SkillManager { } private updateWatchersFromCache(): void { - const desiredPaths = new Set(); - - for (const level of ['project', 'user'] as const) { - const baseDir = this.getSkillsBaseDir(level); - const parentDir = path.dirname(baseDir); - if (fsSync.existsSync(parentDir)) { - desiredPaths.add(parentDir); - } - if (fsSync.existsSync(baseDir)) { - desiredPaths.add(baseDir); - } - - const levelSkills = this.skillsCache?.get(level) || []; - for (const skill of levelSkills) { - const skillDir = path.dirname(skill.filePath); - if (fsSync.existsSync(skillDir)) { - desiredPaths.add(skillDir); - } - } - } + const watchTargets = new Set( + (['project', 'user'] as const) + .map((level) => this.getSkillsBaseDir(level)) + .filter((baseDir) => fsSync.existsSync(baseDir)), + ); for (const existingPath of this.watchers.keys()) { - if (!desiredPaths.has(existingPath)) { + if (!watchTargets.has(existingPath)) { void this.watchers .get(existingPath) ?.close() @@ -522,7 +510,7 @@ export class SkillManager { } } - for (const watchPath of desiredPaths) { + for (const watchPath of watchTargets) { if (this.watchers.has(watchPath)) { continue; } @@ -557,4 +545,26 @@ export class SkillManager { void this.refreshCache().then(() => this.updateWatchersFromCache()); }, 150); } + + private async ensureUserSkillsDir(): Promise { + const baseDir = this.getSkillsBaseDir('user'); + try { + await fs.mkdir(baseDir, { recursive: true }); + } catch (error) { + console.warn( + `Failed to create user skills directory at ${baseDir}:`, + error, + ); + } + } +} + +function normalizeSkillFileContent(content: string): string { + // Strip UTF-8 BOM to ensure frontmatter starts at the first character. + let normalized = content.replace(/^\uFEFF/, ''); + + // Normalize line endings so skills authored on Windows (CRLF) parse correctly. + normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + return normalized; } diff --git a/packages/core/src/tools/skill.ts b/packages/core/src/tools/skill.ts index 93a382fef..f1dc1596b 100644 --- a/packages/core/src/tools/skill.ts +++ b/packages/core/src/tools/skill.ts @@ -53,7 +53,7 @@ export class SkillTool extends BaseDeclarativeTool { false, // canUpdateOutput ); - this.skillManager = config.getSkillManager(); + this.skillManager = config.getSkillManager()!; this.skillManager.addChangeListener(() => { void this.refreshSkills(); }); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 1dd551fd3..28ed2171d 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.7.0", + "version": "0.7.1", "private": true, "main": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/vscode-ide-companion/README.md b/packages/vscode-ide-companion/README.md index 55f7da323..92eb830a6 100644 --- a/packages/vscode-ide-companion/README.md +++ b/packages/vscode-ide-companion/README.md @@ -1,6 +1,11 @@ # Qwen Code Companion -Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visual Studio Code with native IDE features and an intuitive interface. This extension bundles everything you need to get started immediately. +[![Version](https://img.shields.io/visual-studio-marketplace/v/qwenlm.qwen-code-vscode-ide-companion)](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) +[![VS Code Installs](https://img.shields.io/visual-studio-marketplace/i/qwenlm.qwen-code-vscode-ide-companion)](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) +[![Open VSX Downloads](https://img.shields.io/open-vsx/dt/qwenlm/qwen-code-vscode-ide-companion)](https://open-vsx.org/extension/qwenlm/qwen-code-vscode-ide-companion) +[![Rating](https://img.shields.io/visual-studio-marketplace/r/qwenlm.qwen-code-vscode-ide-companion)](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) + +Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visual Studio Code with native IDE features and an intuitive chat interface. This extension bundles everything you need — no additional installation required. ## Demo @@ -11,7 +16,7 @@ Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visua ## Features -- **Native IDE experience**: Dedicated Qwen Code sidebar panel accessed via the Qwen icon +- **Native IDE experience**: Dedicated Qwen Code Chat panel accessed via the Qwen icon in the editor title bar - **Native diffing**: Review, edit, and accept changes in VS Code's diff view - **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 @@ -20,73 +25,46 @@ Seamlessly integrate [Qwen Code](https://github.com/QwenLM/qwen-code) into Visua ## Requirements -- Visual Studio Code 1.85.0 or newer +- Visual Studio Code 1.85.0 or newer (also works with Cursor, Windsurf, and other VS Code-based editors) -## Installation +## Quick Start -1. Install from the VS Code Marketplace: https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion +1. **Install** from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=qwenlm.qwen-code-vscode-ide-companion) or [Open VSX Registry](https://open-vsx.org/extension/qwenlm/qwen-code-vscode-ide-companion) -2. Two ways to use - - Chat panel: Click the Qwen icon in the Activity Bar, or run `Qwen Code: Open` from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`). - - Terminal session (classic): Run `Qwen Code: Run` to launch a session in the integrated terminal (bundled CLI). +2. **Open the Chat panel** using one of these methods: + - Click the **Qwen icon** in the top-right corner of the editor + - Run `Qwen Code: Open` from the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) -## Development and Debugging +3. **Start chatting** — Ask Qwen to help with coding tasks, explain code, fix bugs, or write new features -To debug and develop this extension locally: +## Commands -1. **Clone the repository** +| Command | Description | +| -------------------------------- | ------------------------------------------------------ | +| `Qwen Code: Open` | Open the Qwen Code Chat panel | +| `Qwen Code: Run` | Launch a classic terminal session with the bundled CLI | +| `Qwen Code: Accept Current Diff` | Accept the currently displayed diff | +| `Qwen Code: Close Diff Editor` | Close/reject the current diff | - ```bash - git clone https://github.com/QwenLM/qwen-code.git - cd qwen-code - ``` +## Feedback & Issues -2. **Install dependencies** +- 🐛 [Report bugs](https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&labels=bug,vscode-ide-companion) +- 💡 [Request features](https://github.com/QwenLM/qwen-code/issues/new?template=feature_request.yml&labels=enhancement,vscode-ide-companion) +- 📖 [Documentation](https://qwenlm.github.io/qwen-code-docs/) +- 📋 [Changelog](https://github.com/QwenLM/qwen-code/releases) - ```bash - npm install - # or if using pnpm - pnpm install - ``` +## Contributing -3. **Start debugging** +We welcome contributions! See our [Contributing Guide](https://github.com/QwenLM/qwen-code/blob/main/CONTRIBUTING.md) for details on: - ```bash - code . # Open the project root in VS Code - ``` - - Open the `packages/vscode-ide-companion/src/extension.ts` file - - Open Debug panel (`Ctrl+Shift+D` or `Cmd+Shift+D`) - - Select **"Launch Companion VS Code Extension"** from the debug dropdown - - Press `F5` to launch Extension Development Host - -4. **Make changes and reload** - - Edit the source code in the original VS Code window - - To see your changes, reload the Extension Development Host window by: - - Pressing `Ctrl+R` (Windows/Linux) or `Cmd+R` (macOS) - - Or clicking the "Reload" button in the debug toolbar - -5. **View logs and debug output** - - Open the Debug Console in the original VS Code window to see extension logs - - In the Extension Development Host window, open Developer Tools with `Help > Toggle Developer Tools` to see webview logs - -## Build for Production - -To build the extension for distribution: - -```bash -npm run compile -# or -pnpm run compile -``` - -To package the extension as a VSIX file: - -```bash -npx vsce package -# or -pnpm vsce package -``` +- Setting up the development environment +- Building and debugging the extension locally +- Submitting pull requests ## Terms of Service and Privacy Notice By installing this extension, you agree to the [Terms of Service](https://github.com/QwenLM/qwen-code/blob/main/docs/tos-privacy.md). + +## License + +[Apache-2.0](https://github.com/QwenLM/qwen-code/blob/main/LICENSE) diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index b7c50f57c..6c5be6727 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.7.0", + "version": "0.7.1", "publisher": "qwenlm", "icon": "assets/icon.png", "repository": { diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index be0f669e6..14ff4bcae 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -314,34 +314,32 @@ export async function activate(context: vscode.ExtensionContext) { 'cli.js', ).fsPath; const execPath = process.execPath; - const lowerExecPath = execPath.toLowerCase(); - const needsElectronRunAsNode = - lowerExecPath.includes('code') || - lowerExecPath.includes('electron'); - let qwenCmd: string; const terminalOptions: vscode.TerminalOptions = { name: `Qwen Code (${selectedFolder.name})`, cwd: selectedFolder.uri.fsPath, location, }; + let qwenCmd: string; + if (isWindows) { - // Use system Node via cmd.exe; avoid PowerShell parsing issues + // On Windows, try multiple strategies to find a Node.js runtime: + // 1. Check if VSCode ships a standalone node.exe alongside Code.exe + // 2. Check VSCode's internal Node.js in resources directory + // 3. Fall back to using Code.exe with ELECTRON_RUN_AS_NODE=1 const quoteCmd = (s: string) => `"${s.replace(/"/g, '""')}"`; const cliQuoted = quoteCmd(cliEntry); // TODO: @yiliang114, temporarily run through node, and later hope to decouple from the local node qwenCmd = `node ${cliQuoted}`; terminalOptions.shellPath = process.env.ComSpec; } else { + // macOS/Linux: All VSCode-like IDEs (VSCode, Cursor, Windsurf, etc.) + // are Electron-based, so we always need ELECTRON_RUN_AS_NODE=1 + // to run Node.js scripts using the IDE's bundled runtime. const quotePosix = (s: string) => `"${s.replace(/"/g, '\\"')}"`; const baseCmd = `${quotePosix(execPath)} ${quotePosix(cliEntry)}`; - if (needsElectronRunAsNode) { - // macOS Electron helper needs ELECTRON_RUN_AS_NODE=1; - qwenCmd = `ELECTRON_RUN_AS_NODE=1 ${baseCmd}`; - } else { - qwenCmd = baseCmd; - } + qwenCmd = `ELECTRON_RUN_AS_NODE=1 ${baseCmd}`; } const terminal = vscode.window.createTerminal(terminalOptions);