Compare commits

..

14 Commits

Author SHA1 Message Date
github-actions[bot]
a52e043c00 chore(release): v0.1.0-preview.0 2025-10-24 07:47:11 +00:00
tanzhenxin
bf91aa4e08 fix: skip one more flaky integration test 2025-10-24 14:51:08 +08:00
tanzhenxin
5835db0877 fix: test scripts fail 2025-10-24 14:41:22 +08:00
tanzhenxin
9b86e821bc chore: pump version to 0.1.0, and optimize release workflow 2025-10-24 14:26:58 +08:00
tanzhenxin
d482ad28dd fix: simplify todo-write integration test, to make it more stable 2025-10-24 11:22:44 +08:00
tanzhenxin
570b745f30 fix: add QWEN_OAUTH in interactive mode 2025-10-24 11:12:09 +08:00
tanzhenxin
10f7968bc6 fix: integration test 2025-10-24 10:56:54 +08:00
tanzhenxin
c7f5aaa702 chore: try fix e2e test fail on cloud run 2025-10-23 19:17:01 +08:00
tanzhenxin
5e65d6cae7 chore: upload e2e results to artifacts, for test failure troubleshooting 2025-10-23 18:56:06 +08:00
tanzhenxin
1dd21537ce fix: test case failures on Windows OS 2025-10-23 17:44:15 +08:00
tanzhenxin
23ecddc574 fix: integration tests 2025-10-23 17:27:08 +08:00
tanzhenxin
b9dfff6e83 chore: skip two flaky test cases 2025-10-23 17:11:08 +08:00
tanzhenxin
0fa9b88a14 chore: fix yamllint error 2025-10-23 16:14:17 +08:00
tanzhenxin
ab28db1da8 feat: release qwen-code CLI packages as a standalone bundled js, with necessary vendors 2025-10-23 15:57:16 +08:00
50 changed files with 602 additions and 585 deletions

View File

@@ -167,11 +167,7 @@ jobs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
run: |-
git add package.json package-lock.json packages/*/package.json
if git diff --staged --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(release): ${RELEASE_TAG}"
fi
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${IS_DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags

View File

@@ -66,6 +66,17 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Display all directories added by `/directory add` and `--include-directories`.
- **Usage:** `/directory show`
- **`/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 <path1>,<path2>`
- **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.
@@ -97,20 +108,6 @@ Slash commands provide meta-level control over the CLI itself.
- **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]`

View File

@@ -107,7 +107,7 @@ The `qwen-extension.json` file contains the configuration for the extension. The
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
- Note that all MCP server configuration options are supported except for `trust`.
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the extension directory. If this property is not used but a `QWEN.md` file is present in your extension directory, then that file will be loaded.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. Note that this differs from the MCP server `excludeTools` functionality, which can be listed in the MCP server config. **Important:** Tools specified in `excludeTools` will be disabled for the entire conversation context and will affect all subsequent queries in the current session.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. Note that this differs from the MCP server `excludeTools` functionality, which can be listed in the MCP server config.
When Qwen Code starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.

View File

@@ -2,7 +2,7 @@ export default {
subagents: 'Subagents',
checkpointing: 'Checkpointing',
sandbox: 'Sandbox Support',
headless: 'Headless Mode',
'headless-mode': 'Headless Mode',
'welcome-back': 'Welcome Back',
'token-caching': 'Token Caching',
};

View File

@@ -7,7 +7,7 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { writeFileSync, rmSync } from 'node:fs';
import { writeFileSync } from 'node:fs';
let esbuild;
try {
@@ -22,9 +22,6 @@ const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const pkg = require(path.resolve(__dirname, 'package.json'));
// Clean dist directory (cross-platform)
rmSync(path.resolve(__dirname, 'dist'), { recursive: true, force: true });
const external = [
'@lydell/node-pty',
'node-pty',

View File

@@ -36,10 +36,10 @@ describe('JSON output', () => {
});
it('should return a JSON error for enforced auth mismatch before running', async () => {
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
await rig.setup('json-output-auth-mismatch', {
settings: {
security: { auth: { enforcedType: 'qwen-oauth' } },
security: { auth: { enforcedType: 'gemini-api-key' } },
},
});
@@ -50,7 +50,7 @@ describe('JSON output', () => {
} catch (e) {
thrown = e as Error;
} finally {
delete process.env['OPENAI_API_KEY'];
delete process.env['GOOGLE_GENAI_USE_GCA'];
}
expect(thrown).toBeDefined();
@@ -80,8 +80,10 @@ describe('JSON output', () => {
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
'configured auth type is qwen-oauth',
'configured auth type is gemini-api-key',
);
expect(payload.error.message).toContain(
'current auth type is oauth-personal',
);
expect(payload.error.message).toContain('current auth type is openai');
});
});

View File

@@ -9,6 +9,7 @@ import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { env } from 'node:process';
import { DEFAULT_QWEN_MODEL } from '../packages/core/src/config/models.js';
import fs from 'node:fs';
import { EOL } from 'node:os';
import * as pty from '@lydell/node-pty';
@@ -181,6 +182,7 @@ export class TestRig {
otlpEndpoint: '',
outfile: telemetryPath,
},
model: DEFAULT_QWEN_MODEL,
sandbox: env.GEMINI_SANDBOX !== 'false' ? env.GEMINI_SANDBOX : false,
...options.settings, // Allow tests to override/add settings
};

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"workspaces": [
"packages/*"
],
@@ -16024,7 +16024,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -16139,7 +16139,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -16278,7 +16278,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -16290,7 +16290,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"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.1.2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.0-preview.0"
},
"scripts": {
"start": "cross-env node scripts/start.js",
@@ -28,7 +28,7 @@
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"bundle": "rm -rf dist && npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present --parallel",
"test:ci": "npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.0-preview.0"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -18,26 +18,60 @@ vi.mock('./settings.js', () => ({
describe('validateAuthMethod', () => {
beforeEach(() => {
vi.resetModules();
vi.stubEnv('GEMINI_API_KEY', undefined);
vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined);
vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined);
vi.stubEnv('GOOGLE_API_KEY', undefined);
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should return null for USE_OPENAI', () => {
process.env['OPENAI_API_KEY'] = 'fake-key';
expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();
it('should return null for LOGIN_WITH_GOOGLE', () => {
expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull();
});
it('should return an error message for USE_OPENAI if OPENAI_API_KEY is not set', () => {
delete process.env['OPENAI_API_KEY'];
expect(validateAuthMethod(AuthType.USE_OPENAI)).toBe(
'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.',
);
it('should return null for CLOUD_SHELL', () => {
expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull();
});
it('should return null for QWEN_OAUTH', () => {
expect(validateAuthMethod(AuthType.QWEN_OAUTH)).toBeNull();
describe('USE_GEMINI', () => {
it('should return null if GEMINI_API_KEY is set', () => {
vi.stubEnv('GEMINI_API_KEY', 'test-key');
expect(validateAuthMethod(AuthType.USE_GEMINI)).toBeNull();
});
it('should return an error message if GEMINI_API_KEY is not set', () => {
vi.stubEnv('GEMINI_API_KEY', undefined);
expect(validateAuthMethod(AuthType.USE_GEMINI)).toBe(
'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!',
);
});
});
describe('USE_VERTEX_AI', () => {
it('should return null if GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are set', () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'test-location');
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
});
it('should return null if GOOGLE_API_KEY is set', () => {
vi.stubEnv('GOOGLE_API_KEY', 'test-api-key');
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
});
it('should return an error message if no required environment variables are set', () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined);
vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined);
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBe(
'When using Vertex AI, you must specify either:\n' +
'• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
'Update your environment and try again (no reload needed if using .env)!',
);
});
});
it('should return an error message for an invalid auth method', () => {

View File

@@ -8,13 +8,39 @@ import { AuthType } from '@qwen-code/qwen-code-core';
import { loadEnvironment, loadSettings } from './settings.js';
export function validateAuthMethod(authMethod: string): string | null {
const settings = loadSettings();
loadEnvironment(settings.merged);
loadEnvironment(loadSettings().merged);
if (
authMethod === AuthType.LOGIN_WITH_GOOGLE ||
authMethod === AuthType.CLOUD_SHELL
) {
return null;
}
if (authMethod === AuthType.USE_GEMINI) {
if (!process.env['GEMINI_API_KEY']) {
return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
}
return null;
}
if (authMethod === AuthType.USE_VERTEX_AI) {
const hasVertexProjectLocationConfig =
!!process.env['GOOGLE_CLOUD_PROJECT'] &&
!!process.env['GOOGLE_CLOUD_LOCATION'];
const hasGoogleApiKey = !!process.env['GOOGLE_API_KEY'];
if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) {
return (
'When using Vertex AI, you must specify either:\n' +
'• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
'Update your environment and try again (no reload needed if using .env)!'
);
}
return null;
}
if (authMethod === AuthType.USE_OPENAI) {
const hasApiKey =
process.env['OPENAI_API_KEY'] || settings.merged.security?.auth?.apiKey;
if (!hasApiKey) {
if (!process.env['OPENAI_API_KEY']) {
return 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.';
}
return null;
@@ -28,3 +54,15 @@ export function validateAuthMethod(authMethod: string): string | null {
return 'Invalid auth method selected.';
}
export const setOpenAIApiKey = (apiKey: string): void => {
process.env['OPENAI_API_KEY'] = apiKey;
};
export const setOpenAIBaseUrl = (baseUrl: string): void => {
process.env['OPENAI_BASE_URL'] = baseUrl;
};
export const setOpenAIModel = (model: string): void => {
process.env['OPENAI_MODEL'] = model;
};

View File

@@ -13,6 +13,7 @@ import { extensionsCommand } from '../commands/extensions.js';
import {
ApprovalMode,
Config,
DEFAULT_QWEN_MODEL,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
@@ -194,7 +195,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
.option('proxy', {
type: 'string',
description:
'Proxy for Qwen Code, like schema://user:password@host:port',
'Proxy for gemini client, like schema://user:password@host:port',
})
.deprecateOption(
'proxy',
@@ -668,11 +669,13 @@ export async function loadCliConfig(
);
}
const resolvedModel =
const defaultModel = DEFAULT_QWEN_MODEL;
const resolvedModel: string =
argv.model ||
process.env['OPENAI_MODEL'] ||
process.env['QWEN_MODEL'] ||
settings.model?.name;
settings.model?.name ||
defaultModel;
const sandboxConfig = await loadSandboxConfig(settings, argv);
const screenReader =
@@ -736,14 +739,8 @@ export async function loadCliConfig(
generationConfig: {
...(settings.model?.generationConfig || {}),
model: resolvedModel,
apiKey:
argv.openaiApiKey ||
process.env['OPENAI_API_KEY'] ||
settings.security?.auth?.apiKey,
baseUrl:
argv.openaiBaseUrl ||
process.env['OPENAI_BASE_URL'] ||
settings.security?.auth?.baseUrl,
apiKey: argv.openaiApiKey || process.env['OPENAI_API_KEY'],
baseUrl: argv.openaiBaseUrl || process.env['OPENAI_BASE_URL'],
enableOpenAILogging:
(typeof argv.openaiLogging === 'undefined'
? settings.model?.enableOpenAILogging

View File

@@ -991,24 +991,6 @@ const SETTINGS_SCHEMA = {
description: 'Whether to use an external authentication flow.',
showInDialog: false,
},
apiKey: {
type: 'string',
label: 'API Key',
category: 'Security',
requiresRestart: true,
default: undefined as string | undefined,
description: 'API key for OpenAI compatible authentication.',
showInDialog: false,
},
baseUrl: {
type: 'string',
label: 'Base URL',
category: 'Security',
requiresRestart: true,
default: undefined as string | undefined,
description: 'Base URL for OpenAI compatible API.',
showInDialog: false,
},
},
},
},

View File

@@ -17,7 +17,11 @@ import dns from 'node:dns';
import { randomUUID } from 'node:crypto';
import { start_sandbox } from './utils/sandbox.js';
import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js';
import { loadSettings, migrateDeprecatedSettings } from './config/settings.js';
import {
loadSettings,
migrateDeprecatedSettings,
SettingScope,
} from './config/settings.js';
import { themeManager } from './ui/themes/theme-manager.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
@@ -229,6 +233,17 @@ export async function main() {
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
);
// Set a default auth type if one isn't set.
if (!settings.merged.security?.auth?.selectedType) {
if (process.env['CLOUD_SHELL'] === 'true') {
settings.setValue(
SettingScope.User,
'selectedAuthType',
AuthType.CLOUD_SHELL,
);
}
}
// Load custom themes from settings
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);

View File

@@ -555,7 +555,7 @@ export const AppContainer = (props: AppContainerProps) => {
historyManager.addItem(
{
type: MessageType.INFO,
text: 'Refreshing hierarchical memory (QWEN.md or other context files)...',
text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...',
},
Date.now(),
);

View File

@@ -8,7 +8,12 @@ import type React from 'react';
import { useState } from 'react';
import { AuthType } from '@qwen-code/qwen-code-core';
import { Box, Text } from 'ink';
import { validateAuthMethod } from '../../config/auth.js';
import {
setOpenAIApiKey,
setOpenAIBaseUrl,
setOpenAIModel,
validateAuthMethod,
} from '../../config/auth.js';
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
@@ -16,15 +21,7 @@ import { OpenAIKeyPrompt } from '../components/OpenAIKeyPrompt.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
interface AuthDialogProps {
onSelect: (
authMethod: AuthType | undefined,
scope: SettingScope,
credentials?: {
apiKey?: string;
baseUrl?: string;
model?: string;
},
) => void;
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
settings: LoadedSettings;
initialErrorMessage?: string | null;
}
@@ -73,7 +70,11 @@ export function AuthDialog({
return item.value === defaultAuthType;
}
return item.value === AuthType.QWEN_OAUTH;
if (process.env['GEMINI_API_KEY']) {
return item.value === AuthType.USE_GEMINI;
}
return item.value === AuthType.LOGIN_WITH_GOOGLE;
}),
);
@@ -100,12 +101,11 @@ export function AuthDialog({
baseUrl: string,
model: string,
) => {
setOpenAIApiKey(apiKey);
setOpenAIBaseUrl(baseUrl);
setOpenAIModel(model);
setShowOpenAIKeyPrompt(false);
onSelect(AuthType.USE_OPENAI, SettingScope.User, {
apiKey,
baseUrl,
model,
});
onSelect(AuthType.USE_OPENAI, SettingScope.User);
};
const handleOpenAIKeyCancel = () => {

View File

@@ -6,11 +6,12 @@
import { useState, useCallback, useEffect } from 'react';
import type { LoadedSettings, SettingScope } from '../../config/settings.js';
import type { AuthType, Config } from '@qwen-code/qwen-code-core';
import { AuthType, type Config } from '@qwen-code/qwen-code-core';
import {
clearCachedCredentialFile,
getErrorMessage,
} from '@qwen-code/qwen-code-core';
import { runExitCleanup } from '../../utils/cleanup.js';
import { AuthState } from '../types.js';
import { validateAuthMethod } from '../../config/auth.js';
@@ -46,7 +47,6 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
setAuthError(error);
if (error) {
setAuthState(AuthState.Updating);
setIsAuthDialogOpen(true);
}
},
[setAuthError, setAuthState],
@@ -87,49 +87,24 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
// Handle auth selection from dialog
const handleAuthSelect = useCallback(
async (
authType: AuthType | undefined,
scope: SettingScope,
credentials?: {
apiKey?: string;
baseUrl?: string;
model?: string;
},
) => {
async (authType: AuthType | undefined, scope: SettingScope) => {
if (authType) {
await clearCachedCredentialFile();
// Save OpenAI credentials if provided
if (credentials) {
// Update Config's internal generationConfig before calling refreshAuth
// This ensures refreshAuth has access to the new credentials
config.updateCredentials({
apiKey: credentials.apiKey,
baseUrl: credentials.baseUrl,
model: credentials.model,
});
// Also set environment variables for compatibility with other parts of the code
if (credentials.apiKey) {
settings.setValue(
scope,
'security.auth.apiKey',
credentials.apiKey,
);
}
if (credentials.baseUrl) {
settings.setValue(
scope,
'security.auth.baseUrl',
credentials.baseUrl,
);
}
if (credentials.model) {
settings.setValue(scope, 'model.name', credentials.model);
}
}
settings.setValue(scope, 'security.auth.selectedType', authType);
if (
authType === AuthType.LOGIN_WITH_GOOGLE &&
config.isBrowserLaunchSuppressed()
) {
await runExitCleanup();
console.log(`
----------------------------------------------------------------
Logging in with Google... Please restart Gemini CLI to continue.
----------------------------------------------------------------
`);
process.exit(0);
}
}
setIsAuthDialogOpen(false);

View File

@@ -11,7 +11,6 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
import { getCliVersion } from '../../utils/version.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
import { AuthType } from '@qwen-code/qwen-code-core';
// Mock dependencies
vi.mock('open');
@@ -27,6 +26,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
getDetectedIdeDisplayName: vi.fn().mockReturnValue('VSCode'),
}),
},
sessionId: 'test-session-id',
};
});
vi.mock('node:process', () => ({
@@ -58,16 +58,6 @@ describe('bugCommand', () => {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => undefined,
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -81,7 +71,6 @@ describe('bugCommand', () => {
* **Session ID:** test-session-id
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
@@ -102,16 +91,6 @@ describe('bugCommand', () => {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -125,7 +104,6 @@ describe('bugCommand', () => {
* **Session ID:** test-session-id
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
@@ -136,50 +114,4 @@ describe('bugCommand', () => {
expect(open).toHaveBeenCalledWith(expectedUrl);
});
it('should include Base URL when auth type is OpenAI', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => undefined,
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
getContentGeneratorConfig: () => ({
baseUrl: 'https://api.openai.com/v1',
}),
},
settings: {
merged: {
security: {
auth: {
selectedType: AuthType.USE_OPENAI,
},
},
},
},
},
});
if (!bugCommand.action) throw new Error('Action is not defined');
await bugCommand.action(mockContext, 'OpenAI bug');
const expectedInfo = `
* **CLI Version:** 0.1.0
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** test-session-id
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
* **Auth Type:** ${AuthType.USE_OPENAI}
* **Base URL:** https://api.openai.com/v1
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
`;
const expectedUrl =
'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title=OpenAI%20bug&info=' +
encodeURIComponent(expectedInfo);
expect(open).toHaveBeenCalledWith(expectedUrl);
});
});

View File

@@ -15,7 +15,7 @@ import { MessageType } from '../types.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
import { getCliVersion } from '../../utils/version.js';
import { IdeClient, AuthType } from '@qwen-code/qwen-code-core';
import { IdeClient, sessionId } from '@qwen-code/qwen-code-core';
export const bugCommand: SlashCommand = {
name: 'bug',
@@ -38,24 +38,13 @@ export const bugCommand: SlashCommand = {
const cliVersion = await getCliVersion();
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
const ideClient = await getIdeClientName(context);
const selectedAuthType =
context.services.settings.merged.security?.auth?.selectedType || '';
const baseUrl =
selectedAuthType === AuthType.USE_OPENAI
? config?.getContentGeneratorConfig()?.baseUrl
: undefined;
let info = `
* **CLI Version:** ${cliVersion}
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** ${config?.getSessionId() || 'unknown'}
* **Session ID:** ${sessionId}
* **Operating System:** ${osVersion}
* **Sandbox Environment:** ${sandboxEnv}
* **Auth Type:** ${selectedAuthType}`;
if (baseUrl) {
info += `\n* **Base URL:** ${baseUrl}`;
}
info += `
* **Model Version:** ${modelVersion}
* **Memory Usage:** ${memoryUsage}
`;

View File

@@ -139,8 +139,8 @@ describe('chatCommand', () => {
.match(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
const formattedDate = isoDate ? `${isoDate[1]} ${isoDate[2]}` : '';
expect(content).toContain(formattedDate);
const index1 = content.indexOf('- test1');
const index2 = content.indexOf('- test2');
const index1 = content.indexOf('- \u001b[36mtest1\u001b[0m');
const index2 = content.indexOf('- \u001b[36mtest2\u001b[0m');
expect(index1).toBeGreaterThanOrEqual(0);
expect(index2).toBeGreaterThan(index1);
});

View File

@@ -89,9 +89,9 @@ const listCommand: SlashCommand = {
const isoString = chat.mtime.toISOString();
const match = isoString.match(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date';
message += ` - ${paddedName} (saved on ${formattedDate})\n`;
message += ` - \u001b[36m${paddedName}\u001b[0m \u001b[90m(saved on ${formattedDate})\u001b[0m\n`;
}
message += `\nNote: Newest last, oldest first`;
message += `\n\u001b[90mNote: Newest last, oldest first\u001b[0m`;
return {
type: 'message',
messageType: 'info',

View File

@@ -130,7 +130,7 @@ export function OpenAIKeyPrompt({
}
// Handle regular character input
if (key.sequence && !key.ctrl && !key.meta) {
if (key.sequence && !key.ctrl && !key.meta && !key.name) {
// Filter control characters
const cleanInput = key.sequence
.split('')

View File

@@ -12,7 +12,6 @@ import type {
Config,
} from '@qwen-code/qwen-code-core';
import { renderWithProviders } from '../../../test-utils/render.js';
import type { LoadedSettings } from '../../../config/settings.js';
describe('ToolConfirmationMessage', () => {
const mockConfig = {
@@ -188,63 +187,4 @@ describe('ToolConfirmationMessage', () => {
});
});
});
describe('external editor option', () => {
const editConfirmationDetails: ToolCallConfirmationDetails = {
type: 'edit',
title: 'Confirm Edit',
fileName: 'test.txt',
filePath: '/test.txt',
fileDiff: '...diff...',
originalContent: 'a',
newContent: 'b',
onConfirm: vi.fn(),
};
it('should show "Modify with external editor" when preferredEditor is set', () => {
const mockConfig = {
isTrustedFolder: () => true,
getIdeMode: () => false,
} as unknown as Config;
const { lastFrame } = renderWithProviders(
<ToolConfirmationMessage
confirmationDetails={editConfirmationDetails}
config={mockConfig}
availableTerminalHeight={30}
terminalWidth={80}
/>,
{
settings: {
merged: { general: { preferredEditor: 'vscode' } },
} as unknown as LoadedSettings,
},
);
expect(lastFrame()).toContain('Modify with external editor');
});
it('should NOT show "Modify with external editor" when preferredEditor is not set', () => {
const mockConfig = {
isTrustedFolder: () => true,
getIdeMode: () => false,
} as unknown as Config;
const { lastFrame } = renderWithProviders(
<ToolConfirmationMessage
confirmationDetails={editConfirmationDetails}
config={mockConfig}
availableTerminalHeight={30}
terminalWidth={80}
/>,
{
settings: {
merged: { general: {} },
} as unknown as LoadedSettings,
},
);
expect(lastFrame()).not.toContain('Modify with external editor');
});
});
});

View File

@@ -15,14 +15,12 @@ import type {
ToolExecuteConfirmationDetails,
ToolMcpConfirmationDetails,
Config,
EditorType,
} from '@qwen-code/qwen-code-core';
import { IdeClient, ToolConfirmationOutcome } from '@qwen-code/qwen-code-core';
import type { RadioSelectItem } from '../shared/RadioButtonSelect.js';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { useKeypress } from '../../hooks/useKeypress.js';
import { useSettings } from '../../contexts/SettingsContext.js';
import { theme } from '../../semantic-colors.js';
export interface ToolConfirmationMessageProps {
@@ -47,11 +45,6 @@ export const ToolConfirmationMessage: React.FC<
const { onConfirm } = confirmationDetails;
const childWidth = terminalWidth - 2; // 2 for padding
const settings = useSettings();
const preferredEditor = settings.merged.general?.preferredEditor as
| EditorType
| undefined;
const [ideClient, setIdeClient] = useState<IdeClient | null>(null);
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
@@ -206,7 +199,7 @@ export const ToolConfirmationMessage: React.FC<
key: 'Yes, allow always',
});
}
if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) {
if (!config.getIdeMode() || !isDiffingEnabled) {
options.push({
label: 'Modify with external editor',
value: ToolConfirmationOutcome.ModifyWithEditor,

View File

@@ -109,7 +109,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);
@@ -139,7 +139,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
undefined,
);
@@ -170,7 +170,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);
@@ -199,7 +199,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);

View File

@@ -45,7 +45,7 @@ export const useEditorSettings = (
}
try {
loadedSettings.setValue(scope, 'general.preferredEditor', editorType);
loadedSettings.setValue(scope, 'preferredEditor', editorType);
addItem(
{
type: MessageType.INFO,

View File

@@ -20,14 +20,12 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
{
id: MAINLINE_CODER,
label: MAINLINE_CODER,
description:
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
description: 'Optimized for code generation and understanding',
},
{
id: MAINLINE_VLM,
label: MAINLINE_VLM,
description:
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
description: 'Vision model with multimodal capabilities',
isVision: true,
},
];

View File

@@ -105,6 +105,34 @@ describe('validateNonInterActiveAuth', () => {
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
});
it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('uses USE_OPENAI if OPENAI_API_KEY is set', async () => {
process.env['OPENAI_API_KEY'] = 'fake-openai-key';
const nonInteractiveConfig = {
@@ -140,6 +168,104 @@ describe('validateNonInterActiveAuth', () => {
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_API_KEY'] = 'vertex-api-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set, even with other env vars', async () => {
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
});
it('uses USE_VERTEX_AI if both GEMINI_API_KEY and GOOGLE_GENAI_USE_VERTEXAI are set', async () => {
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
});
it('uses USE_GEMINI if GOOGLE_GENAI_USE_VERTEXAI is false, GEMINI_API_KEY is set, and project/location are available', async () => {
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'false';
process.env['GEMINI_API_KEY'] = 'fake-key';
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
undefined,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('uses configuredAuthType if provided', async () => {
// Set required env var for USE_GEMINI
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
@@ -191,25 +317,26 @@ describe('validateNonInterActiveAuth', () => {
});
it('uses enforcedAuthType if provided', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_OPENAI;
mockSettings.merged.security!.auth!.selectedType = AuthType.USE_OPENAI;
// Set required env var for USE_OPENAI to ensure enforcedAuthType takes precedence
process.env['OPENAI_API_KEY'] = 'fake-key';
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
mockSettings.merged.security!.auth!.selectedType = AuthType.USE_GEMINI;
// Set required env var for USE_GEMINI to ensure enforcedAuthType takes precedence
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('exits if currentAuthType does not match enforcedAuthType', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
mockSettings.merged.security!.auth!.enforcedType =
AuthType.LOGIN_WITH_GOOGLE;
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
@@ -219,7 +346,7 @@ describe('validateNonInterActiveAuth', () => {
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
@@ -229,7 +356,7 @@ describe('validateNonInterActiveAuth', () => {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The configured auth type is qwen-oauth, but the current auth type is openai. Please re-authenticate with the correct type.',
'The configured auth type is oauth-personal, but the current auth type is vertex-ai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
@@ -267,8 +394,8 @@ describe('validateNonInterActiveAuth', () => {
});
it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -297,14 +424,14 @@ describe('validateNonInterActiveAuth', () => {
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
'The configured auth type is qwen-oauth, but the current auth type is openai.',
'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.',
);
}
});
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['OPENAI_API_KEY'] = 'fake-key';
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -317,7 +444,7 @@ describe('validateNonInterActiveAuth', () => {
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,

View File

@@ -12,13 +12,21 @@ import { type LoadedSettings } from './config/settings.js';
import { handleError } from './utils/errors.js';
function getAuthTypeFromEnv(): AuthType | undefined {
if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') {
return AuthType.LOGIN_WITH_GOOGLE;
}
if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') {
return AuthType.USE_VERTEX_AI;
}
if (process.env['GEMINI_API_KEY']) {
return AuthType.USE_GEMINI;
}
if (process.env['OPENAI_API_KEY']) {
return AuthType.USE_OPENAI;
}
if (process.env['QWEN_OAUTH']) {
return AuthType.QWEN_OAUTH;
}
return undefined;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -16,7 +16,6 @@ import {
QwenLogger,
} from '../telemetry/index.js';
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
import {
AuthType,
createContentGeneratorConfig,
@@ -251,7 +250,6 @@ describe('Server Config (config.ts)', () => {
authType,
{
model: MODEL,
baseUrl: DEFAULT_DASHSCOPE_BASE_URL,
},
);
// Verify that contentGeneratorConfig is updated

View File

@@ -88,9 +88,8 @@ import {
DEFAULT_FILE_FILTERING_OPTIONS,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
} from './constants.js';
import { DEFAULT_QWEN_EMBEDDING_MODEL, DEFAULT_QWEN_MODEL } from './models.js';
import { DEFAULT_QWEN_EMBEDDING_MODEL } from './models.js';
import { Storage } from './storage.js';
import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
// Re-export types
export type { AnyToolInvocation, FileFilteringOptions, MCPOAuthConfig };
@@ -244,7 +243,7 @@ export interface ConfigParameters {
fileDiscoveryService?: FileDiscoveryService;
includeDirectories?: string[];
bugCommand?: BugCommandSettings;
model?: string;
model: string;
extensionContextFilePaths?: string[];
maxSessionTurns?: number;
sessionTokenLimit?: number;
@@ -290,7 +289,7 @@ export class Config {
private fileSystemService: FileSystemService;
private contentGeneratorConfig!: ContentGeneratorConfig;
private contentGenerator!: ContentGenerator;
private _generationConfig: Partial<ContentGeneratorConfig>;
private readonly _generationConfig: ContentGeneratorConfig;
private readonly embeddingModel: string;
private readonly sandbox: SandboxConfig | undefined;
private readonly targetDir: string;
@@ -441,10 +440,8 @@ export class Config {
this._generationConfig = {
model: params.model,
...(params.generationConfig || {}),
baseUrl: params.generationConfig?.baseUrl || DEFAULT_DASHSCOPE_BASE_URL,
};
this.contentGeneratorConfig = this
._generationConfig as ContentGeneratorConfig;
this.contentGeneratorConfig = this._generationConfig;
this.cliVersion = params.cliVersion;
this.loadMemoryFromIncludeDirectories =
@@ -523,26 +520,6 @@ export class Config {
return this.contentGenerator;
}
/**
* Updates the credentials in the generation config.
* This is needed when credentials are set after Config construction.
*/
updateCredentials(credentials: {
apiKey?: string;
baseUrl?: string;
model?: string;
}): void {
if (credentials.apiKey) {
this._generationConfig.apiKey = credentials.apiKey;
}
if (credentials.baseUrl) {
this._generationConfig.baseUrl = credentials.baseUrl;
}
if (credentials.model) {
this._generationConfig.model = credentials.model;
}
}
async refreshAuth(authMethod: AuthType) {
// Vertex and Genai have incompatible encryption and sending history with
// throughtSignature from Genai to Vertex will fail, we need to strip them
@@ -610,7 +587,7 @@ export class Config {
}
getModel(): string {
return this.contentGeneratorConfig?.model || DEFAULT_QWEN_MODEL;
return this.contentGeneratorConfig.model;
}
async setModel(

View File

@@ -4,9 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { ContentGenerator } from './contentGenerator.js';
import { createContentGenerator, AuthType } from './contentGenerator.js';
import {
createContentGenerator,
AuthType,
createContentGeneratorConfig,
} from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { GoogleGenAI } from '@google/genai';
import type { Config } from '../config/config.js';
@@ -106,3 +110,83 @@ describe('createContentGenerator', () => {
);
});
});
describe('createContentGeneratorConfig', () => {
const mockConfig = {
getModel: vi.fn().mockReturnValue('gemini-pro'),
setModel: vi.fn(),
flashFallbackHandler: vi.fn(),
getProxy: vi.fn(),
getEnableOpenAILogging: vi.fn().mockReturnValue(false),
getSamplingParams: vi.fn().mockReturnValue(undefined),
getContentGeneratorTimeout: vi.fn().mockReturnValue(undefined),
getContentGeneratorMaxRetries: vi.fn().mockReturnValue(undefined),
getContentGeneratorDisableCacheControl: vi.fn().mockReturnValue(undefined),
getContentGeneratorSamplingParams: vi.fn().mockReturnValue(undefined),
getCliVersion: vi.fn().mockReturnValue('1.0.0'),
} as unknown as Config;
beforeEach(() => {
// Reset modules to re-evaluate imports and environment variables
vi.resetModules();
vi.clearAllMocks();
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should configure for Gemini using GEMINI_API_KEY when set', async () => {
vi.stubEnv('GEMINI_API_KEY', 'env-gemini-key');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.USE_GEMINI,
);
expect(config.apiKey).toBe('env-gemini-key');
expect(config.vertexai).toBe(false);
});
it('should not configure for Gemini if GEMINI_API_KEY is empty', async () => {
vi.stubEnv('GEMINI_API_KEY', '');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.USE_GEMINI,
);
expect(config.apiKey).toBeUndefined();
expect(config.vertexai).toBeUndefined();
});
it('should configure for Vertex AI using GOOGLE_API_KEY when set', async () => {
vi.stubEnv('GOOGLE_API_KEY', 'env-google-key');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.USE_VERTEX_AI,
);
expect(config.apiKey).toBe('env-google-key');
expect(config.vertexai).toBe(true);
});
it('should configure for Vertex AI using GCP project and location when set', async () => {
vi.stubEnv('GOOGLE_API_KEY', undefined);
vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'env-gcp-project');
vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'env-gcp-location');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.USE_VERTEX_AI,
);
expect(config.vertexai).toBe(true);
expect(config.apiKey).toBeUndefined();
});
it('should not configure for Vertex AI if required env vars are empty', async () => {
vi.stubEnv('GOOGLE_API_KEY', '');
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
vi.stubEnv('GOOGLE_CLOUD_LOCATION', '');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.USE_VERTEX_AI,
);
expect(config.apiKey).toBeUndefined();
expect(config.vertexai).toBeUndefined();
});
});

View File

@@ -14,8 +14,8 @@ import type {
} from '@google/genai';
import { GoogleGenAI } from '@google/genai';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import type { Config } from '../config/config.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import type { UserTierId } from '../code_assist/types.js';
import { InstallationManager } from '../utils/installationManager.js';
@@ -82,37 +82,53 @@ export function createContentGeneratorConfig(
authType: AuthType | undefined,
generationConfig?: Partial<ContentGeneratorConfig>,
): ContentGeneratorConfig {
const newContentGeneratorConfig: Partial<ContentGeneratorConfig> = {
const geminiApiKey = process.env['GEMINI_API_KEY'] || undefined;
const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined;
const googleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'] || undefined;
const googleCloudLocation = process.env['GOOGLE_CLOUD_LOCATION'] || undefined;
const newContentGeneratorConfig: ContentGeneratorConfig = {
...(generationConfig || {}),
model: generationConfig?.model || DEFAULT_QWEN_MODEL,
authType,
proxy: config?.getProxy(),
};
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
if (
authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.CLOUD_SHELL
) {
return newContentGeneratorConfig;
}
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
newContentGeneratorConfig.apiKey = geminiApiKey;
newContentGeneratorConfig.vertexai = false;
return newContentGeneratorConfig;
}
if (
authType === AuthType.USE_VERTEX_AI &&
(googleApiKey || (googleCloudProject && googleCloudLocation))
) {
newContentGeneratorConfig.apiKey = googleApiKey;
newContentGeneratorConfig.vertexai = true;
return newContentGeneratorConfig;
}
if (authType === AuthType.QWEN_OAUTH) {
// For Qwen OAuth, we'll handle the API key dynamically in createContentGenerator
// Set a special marker to indicate this is Qwen OAuth
return {
...newContentGeneratorConfig,
model: DEFAULT_QWEN_MODEL,
apiKey: 'QWEN_OAUTH_DYNAMIC_TOKEN',
} as ContentGeneratorConfig;
newContentGeneratorConfig.apiKey = 'QWEN_OAUTH_DYNAMIC_TOKEN';
newContentGeneratorConfig.model = DEFAULT_QWEN_MODEL;
return newContentGeneratorConfig;
}
if (authType === AuthType.USE_OPENAI) {
if (!newContentGeneratorConfig.apiKey) {
throw new Error('OpenAI API key is required');
}
return {
...newContentGeneratorConfig,
model: newContentGeneratorConfig?.model || 'qwen3-coder-plus',
} as ContentGeneratorConfig;
}
return {
...newContentGeneratorConfig,
model: newContentGeneratorConfig?.model || DEFAULT_QWEN_MODEL,
} as ContentGeneratorConfig;
return newContentGeneratorConfig;
}
export async function createContentGenerator(

View File

@@ -1,8 +1,2 @@
export const DEFAULT_TIMEOUT = 120000;
export const DEFAULT_MAX_RETRIES = 3;
export const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
export const DEFAULT_DASHSCOPE_BASE_URL =
'https://dashscope.aliyuncs.com/compatible-mode/v1';
export const DEFAULT_DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
export const DEFAULT_OPEN_ROUTER_BASE_URL = 'https://openrouter.ai/api/v1';

View File

@@ -7,7 +7,6 @@
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';
describe('OpenAIContentConverter', () => {
let converter: OpenAIContentConverter;
@@ -69,77 +68,4 @@ describe('OpenAIContentConverter', () => {
expect(parser.getBuffer(0)).toBe('');
});
});
describe('convertGeminiRequestToOpenAI', () => {
const createRequestWithFunctionResponse = (
response: Record<string, unknown>,
): GenerateContentParameters => {
const contents: Content[] = [
{
role: 'model',
parts: [
{
functionCall: {
id: 'call_1',
name: 'shell',
args: {},
},
},
],
},
{
role: 'user',
parts: [
{
functionResponse: {
id: 'call_1',
name: 'shell',
response,
},
},
],
},
];
return {
model: 'models/test',
contents,
};
};
it('should extract raw output from function response objects', () => {
const request = createRequestWithFunctionResponse({
output: 'Raw output text',
});
const messages = converter.convertGeminiRequestToOpenAI(request);
const toolMessage = messages.find((message) => message.role === 'tool');
expect(toolMessage).toBeDefined();
expect(toolMessage?.content).toBe('Raw output text');
});
it('should prioritize error field when present', () => {
const request = createRequestWithFunctionResponse({
error: 'Command failed',
});
const messages = converter.convertGeminiRequestToOpenAI(request);
const toolMessage = messages.find((message) => message.role === 'tool');
expect(toolMessage).toBeDefined();
expect(toolMessage?.content).toBe('Command failed');
});
it('should stringify non-string responses', () => {
const request = createRequestWithFunctionResponse({
data: { value: 42 },
});
const messages = converter.convertGeminiRequestToOpenAI(request);
const toolMessage = messages.find((message) => message.role === 'tool');
expect(toolMessage).toBeDefined();
expect(toolMessage?.content).toBe('{"data":{"value":42}}');
});
});
});

View File

@@ -276,7 +276,10 @@ export class OpenAIContentConverter {
messages.push({
role: 'tool' as const,
tool_call_id: funcResponse.id || '',
content: this.extractFunctionResponseContent(funcResponse.response),
content:
typeof funcResponse.response === 'string'
? funcResponse.response
: JSON.stringify(funcResponse.response),
});
}
return;
@@ -356,36 +359,6 @@ export class OpenAIContentConverter {
return { textParts, functionCalls, functionResponses, mediaParts };
}
private extractFunctionResponseContent(response: unknown): string {
if (response === null || response === undefined) {
return '';
}
if (typeof response === 'string') {
return response;
}
if (typeof response === 'object') {
const responseObject = response as Record<string, unknown>;
const output = responseObject['output'];
if (typeof output === 'string') {
return output;
}
const error = responseObject['error'];
if (typeof error === 'string') {
return error;
}
}
try {
const serialized = JSON.stringify(response);
return serialized ?? String(response);
} catch {
return String(response);
}
}
/**
* Determine media type from MIME type
*/

View File

@@ -2,11 +2,7 @@ import OpenAI from 'openai';
import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { AuthType } from '../../contentGenerator.js';
import {
DEFAULT_TIMEOUT,
DEFAULT_MAX_RETRIES,
DEFAULT_DASHSCOPE_BASE_URL,
} from '../constants.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import { tokenLimit } from '../../tokenLimits.js';
import type {
OpenAICompatibleProvider,
@@ -57,7 +53,7 @@ export class DashScopeOpenAICompatibleProvider
buildClient(): OpenAI {
const {
apiKey,
baseUrl = DEFAULT_DASHSCOPE_BASE_URL,
baseUrl,
timeout = DEFAULT_TIMEOUT,
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;

View File

@@ -8,7 +8,7 @@ import { OpenAIContentGenerator } from '../core/openaiContentGenerator/index.js'
import { DashScopeOpenAICompatibleProvider } from '../core/openaiContentGenerator/provider/dashscope.js';
import type { IQwenOAuth2Client } from './qwenOAuth2.js';
import { SharedTokenManager } from './sharedTokenManager.js';
import { type Config } from '../config/config.js';
import type { Config } from '../config/config.js';
import type {
GenerateContentParameters,
GenerateContentResponse,
@@ -18,7 +18,10 @@ import type {
EmbedContentResponse,
} from '@google/genai';
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
// Default fallback base URL if no endpoint is provided
const DEFAULT_QWEN_BASE_URL =
'https://dashscope.aliyuncs.com/compatible-mode/v1';
/**
* Qwen Content Generator that uses Qwen OAuth tokens with automatic refresh
@@ -55,7 +58,7 @@ export class QwenContentGenerator extends OpenAIContentGenerator {
* Get the current endpoint URL with proper protocol and /v1 suffix
*/
private getCurrentEndpoint(resourceUrl?: string): string {
const baseEndpoint = resourceUrl || DEFAULT_DASHSCOPE_BASE_URL;
const baseEndpoint = resourceUrl || DEFAULT_QWEN_BASE_URL;
const suffix = '/v1';
// Normalize the URL: add protocol if missing, ensure /v1 suffix

View File

@@ -131,14 +131,16 @@ describe('ExitPlanModeTool', () => {
}
const result = await invocation.execute(signal);
const expectedLlmMessage =
'User has approved your plan. You can now start coding. Start with updating your todo list if applicable.';
expect(result.llmContent).toContain(
'User has approved your plan. You can now start coding',
);
expect(result.returnDisplay).toEqual({
type: 'plan_summary',
message: 'User approved the plan.',
plan: params.plan,
expect(result).toEqual({
llmContent: expectedLlmMessage,
returnDisplay: {
type: 'plan_summary',
message: 'User approved the plan.',
plan: params.plan,
},
});
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
@@ -186,12 +188,15 @@ describe('ExitPlanModeTool', () => {
const result = await invocation.execute(signal);
expect(result.llmContent).toBe(
'Plan execution was not approved. Remaining in plan mode.',
);
expect(result.returnDisplay).toBe(
'Plan execution was not approved. Remaining in plan mode.',
);
expect(result).toEqual({
llmContent: JSON.stringify({
success: false,
plan: params.plan,
error: 'Plan execution was not approved. Remaining in plan mode.',
}),
returnDisplay:
'Plan execution was not approved. Remaining in plan mode.',
});
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
@@ -210,6 +215,50 @@ describe('ExitPlanModeTool', () => {
);
});
it('should handle execution errors gracefully', async () => {
const params: ExitPlanModeParams = {
plan: 'Test plan',
};
const invocation = tool.build(params);
const confirmation = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
if (confirmation) {
// Don't approve the plan so we go through the rejection path
await confirmation.onConfirm(ToolConfirmationOutcome.Cancel);
}
// Create a spy to simulate an error during the execution
const consoleSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
// Mock JSON.stringify to throw an error in the rejection path
const originalStringify = JSON.stringify;
vi.spyOn(JSON, 'stringify').mockImplementationOnce(() => {
throw new Error('JSON stringify error');
});
const result = await invocation.execute(new AbortController().signal);
expect(result).toEqual({
llmContent: JSON.stringify({
success: false,
error: 'Failed to present plan. Detail: JSON stringify error',
}),
returnDisplay: 'Error presenting plan: JSON stringify error',
});
expect(consoleSpy).toHaveBeenCalledWith(
'[ExitPlanModeTool] Error executing exit_plan_mode: JSON stringify error',
);
// Restore original JSON.stringify
JSON.stringify = originalStringify;
consoleSpy.mockRestore();
});
it('should return empty tool locations', () => {
const params: ExitPlanModeParams = {
plan: 'Test plan',

View File

@@ -115,12 +115,17 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
const rejectionMessage =
'Plan execution was not approved. Remaining in plan mode.';
return {
llmContent: rejectionMessage,
llmContent: JSON.stringify({
success: false,
plan,
error: rejectionMessage,
}),
returnDisplay: rejectionMessage,
};
}
const llmMessage = `User has approved your plan. You can now start coding. Start with updating your todo list if applicable.`;
const llmMessage =
'User has approved your plan. You can now start coding. Start with updating your todo list if applicable.';
const displayMessage = 'User approved the plan.';
return {
@@ -137,11 +142,11 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
console.error(
`[ExitPlanModeTool] Error executing exit_plan_mode: ${errorMessage}`,
);
const errorLlmContent = `Failed to present plan: ${errorMessage}`;
return {
llmContent: errorLlmContent,
llmContent: JSON.stringify({
success: false,
error: `Failed to present plan. Detail: ${errorMessage}`,
}),
returnDisplay: `Error presenting plan: ${errorMessage}`,
};
}

View File

@@ -241,7 +241,9 @@ describe('MemoryTool', () => {
expectedFsArgument,
);
const successMessage = `Okay, I've remembered that in global memory: "${params.fact}"`;
expect(result.llmContent).toBe(successMessage);
expect(result.llmContent).toBe(
JSON.stringify({ success: true, message: successMessage }),
);
expect(result.returnDisplay).toBe(successMessage);
});
@@ -269,7 +271,9 @@ describe('MemoryTool', () => {
expectedFsArgument,
);
const successMessage = `Okay, I've remembered that in project memory: "${params.fact}"`;
expect(result.llmContent).toBe(successMessage);
expect(result.llmContent).toBe(
JSON.stringify({ success: true, message: successMessage }),
);
expect(result.returnDisplay).toBe(successMessage);
});
@@ -294,7 +298,10 @@ describe('MemoryTool', () => {
const result = await invocation.execute(mockAbortSignal);
expect(result.llmContent).toBe(
`Error saving memory: ${underlyingError.message}`,
JSON.stringify({
success: false,
error: `Failed to save memory. Detail: ${underlyingError.message}`,
}),
);
expect(result.returnDisplay).toBe(
`Error saving memory: ${underlyingError.message}`,
@@ -312,8 +319,6 @@ describe('MemoryTool', () => {
expect(result.llmContent).toContain(
'Please specify where to save this memory',
);
expect(result.llmContent).toContain('Global:');
expect(result.llmContent).toContain('Project:');
expect(result.returnDisplay).toContain('Global:');
expect(result.returnDisplay).toContain('Project:');
});

View File

@@ -309,7 +309,7 @@ Preview of changes to be made to GLOBAL memory:
if (!fact || typeof fact !== 'string' || fact.trim() === '') {
const errorMessage = 'Parameter "fact" must be a non-empty string.';
return {
llmContent: `Error: ${errorMessage}`,
llmContent: JSON.stringify({ success: false, error: errorMessage }),
returnDisplay: `Error: ${errorMessage}`,
};
}
@@ -324,7 +324,10 @@ Global: ${globalPath} (shared across all projects)
Project: ${projectPath} (current project only)`;
return {
llmContent: errorMessage,
llmContent: JSON.stringify({
success: false,
error: 'Please specify where to save this memory',
}),
returnDisplay: errorMessage,
};
}
@@ -341,7 +344,10 @@ Project: ${projectPath} (current project only)`;
await fs.writeFile(memoryFilePath, modified_content, 'utf-8');
const successMessage = `Okay, I've updated the ${scope} memory file with your modifications.`;
return {
llmContent: successMessage,
llmContent: JSON.stringify({
success: true,
message: successMessage,
}),
returnDisplay: successMessage,
};
} else {
@@ -353,7 +359,10 @@ Project: ${projectPath} (current project only)`;
});
const successMessage = `Okay, I've remembered that in ${scope} memory: "${fact}"`;
return {
llmContent: successMessage,
llmContent: JSON.stringify({
success: true,
message: successMessage,
}),
returnDisplay: successMessage,
};
}
@@ -363,9 +372,11 @@ Project: ${projectPath} (current project only)`;
console.error(
`[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`,
);
return {
llmContent: `Error saving memory: ${errorMessage}`,
llmContent: JSON.stringify({
success: false,
error: `Failed to save memory. Detail: ${errorMessage}`,
}),
returnDisplay: `Error saving memory: ${errorMessage}`,
error: {
message: errorMessage,

View File

@@ -141,12 +141,7 @@ describe('TodoWriteTool', () => {
const invocation = tool.build(params);
const result = await invocation.execute(mockAbortSignal);
expect(result.llmContent).toContain(
'Todos have been modified successfully',
);
expect(result.llmContent).toContain('<system-reminder>');
expect(result.llmContent).toContain('Your todo list has changed');
expect(result.llmContent).toContain(JSON.stringify(params.todos));
expect(result.llmContent).toContain('success');
expect(result.returnDisplay).toEqual({
type: 'todo_list',
todos: [
@@ -183,12 +178,7 @@ describe('TodoWriteTool', () => {
const invocation = tool.build(params);
const result = await invocation.execute(mockAbortSignal);
expect(result.llmContent).toContain(
'Todos have been modified successfully',
);
expect(result.llmContent).toContain('<system-reminder>');
expect(result.llmContent).toContain('Your todo list has changed');
expect(result.llmContent).toContain(JSON.stringify(params.todos));
expect(result.llmContent).toContain('success');
expect(result.returnDisplay).toEqual({
type: 'todo_list',
todos: [
@@ -218,10 +208,7 @@ describe('TodoWriteTool', () => {
const invocation = tool.build(params);
const result = await invocation.execute(mockAbortSignal);
expect(result.llmContent).toContain('Failed to modify todos');
expect(result.llmContent).toContain('<system-reminder>');
expect(result.llmContent).toContain('Todo list modification failed');
expect(result.llmContent).toContain('Write failed');
expect(result.llmContent).toContain('"success":false');
expect(result.returnDisplay).toContain('Error writing todos');
});
@@ -236,10 +223,7 @@ describe('TodoWriteTool', () => {
const invocation = tool.build(params);
const result = await invocation.execute(mockAbortSignal);
expect(result.llmContent).toContain('Todo list has been cleared');
expect(result.llmContent).toContain('<system-reminder>');
expect(result.llmContent).toContain('Your todo list is now empty');
expect(result.llmContent).toContain('no pending tasks');
expect(result.llmContent).toContain('success');
expect(result.returnDisplay).toEqual({
type: 'todo_list',
todos: [],

View File

@@ -340,30 +340,11 @@ class TodoWriteToolInvocation extends BaseToolInvocation<
todos: finalTodos,
};
// Create plain string format with system reminder
const todosJson = JSON.stringify(finalTodos);
let llmContent: string;
if (finalTodos.length === 0) {
// Special message for empty todos
llmContent = `Todo list has been cleared.
<system-reminder>
Your todo list is now empty. DO NOT mention this explicitly to the user. You have no pending tasks in your todo list.
</system-reminder>`;
} else {
// Normal message for todos with items
llmContent = `Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
<system-reminder>
Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:
${todosJson}. Continue on with the tasks at hand if applicable.
</system-reminder>`;
}
return {
llmContent,
llmContent: JSON.stringify({
success: true,
todos: finalTodos,
}),
returnDisplay: todoResultDisplay,
};
} catch (error) {
@@ -372,16 +353,11 @@ ${todosJson}. Continue on with the tasks at hand if applicable.
console.error(
`[TodoWriteTool] Error executing todo_write: ${errorMessage}`,
);
// Create plain string format for error with system reminder
const errorLlmContent = `Failed to modify todos. An error occurred during the operation.
<system-reminder>
Todo list modification failed with error: ${errorMessage}. You may need to retry or handle this error appropriately.
</system-reminder>`;
return {
llmContent: errorLlmContent,
llmContent: JSON.stringify({
success: false,
error: `Failed to write todos. Detail: ${errorMessage}`,
}),
returnDisplay: `Error writing todos: ${errorMessage}`,
};
}

View File

@@ -339,7 +339,6 @@ describe('editor utils', () => {
diffCommand.args,
{
stdio: 'inherit',
shell: process.platform === 'win32',
},
);
expect(mockSpawnOn).toHaveBeenCalledWith('close', expect.any(Function));

View File

@@ -195,7 +195,6 @@ export async function openDiff(
return new Promise<void>((resolve, reject) => {
const childProcess = spawn(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
shell: process.platform === 'win32',
});
childProcess.on('close', (code) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.2",
"version": "0.1.0-preview.0",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -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.1.2",
"version": "0.1.0-preview.0",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {