Compare commits

...

6 Commits

Author SHA1 Message Date
mingholy.lmh
0afc50970f Merge branch 'main' of github.com:QwenLM/qwen-code into mingholy/fix/acp-slashcommands 2025-11-18 13:23:12 +08:00
yyyanghj
0eeffc6875 feat: add support for Trae editor (#1037) 2025-11-17 10:58:33 +08:00
hj C
f0e21374c1 feat: add support for alternative cached_tokens format in OpenAI converter (#1035)
Co-authored-by: chenhuanjie <chenhuanjie@xiaohongshu.com>
2025-11-14 18:09:33 +08:00
BlockHand
29261c75e1 feat: openApi configurable window (#1019) 2025-11-14 10:18:57 +08:00
tanzhenxin
b4eba6584a chore: pump version to 0.2.2 (#1027) 2025-11-13 20:39:14 +08:00
mingholy.lmh
11ae56c18a fix: basic slash command support 2025-11-13 10:32:28 +08:00
18 changed files with 343 additions and 44 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.2.1",
"version": "0.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.2.1",
"version": "0.2.2",
"workspaces": [
"packages/*"
],
@@ -16024,7 +16024,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.2.1",
"version": "0.2.2",
"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.2.1",
"version": "0.2.2",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -16278,7 +16278,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.2.1",
"version": "0.2.2",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -16290,7 +16290,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.2.1",
"version": "0.2.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.2.1",
"version": "0.2.2",
"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.2.1"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.2.2"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

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

View File

@@ -23,6 +23,7 @@ import type { Part } from '@google/genai';
import { runNonInteractive } from './nonInteractiveCli.js';
import { vi } from 'vitest';
import type { LoadedSettings } from './config/settings.js';
import { CommandKind } from './ui/commands/types.js';
// Mock core modules
vi.mock('./ui/hooks/atCommandProcessor.js');
@@ -727,6 +728,7 @@ describe('runNonInteractive', () => {
const mockCommand = {
name: 'testcommand',
description: 'a test command',
kind: CommandKind.FILE,
action: vi.fn().mockResolvedValue({
type: 'submit_prompt',
content: [{ text: 'Prompt from command' }],
@@ -766,6 +768,7 @@ describe('runNonInteractive', () => {
const mockCommand = {
name: 'confirm',
description: 'a command that needs confirmation',
kind: CommandKind.FILE,
action: vi.fn().mockResolvedValue({
type: 'confirm_shell_commands',
commands: ['rm -rf /'],
@@ -821,6 +824,7 @@ describe('runNonInteractive', () => {
const mockCommand = {
name: 'noaction',
description: 'unhandled type',
kind: CommandKind.FILE,
action: vi.fn().mockResolvedValue({
type: 'unhandled',
}),
@@ -847,6 +851,7 @@ describe('runNonInteractive', () => {
const mockCommand = {
name: 'testargs',
description: 'a test command',
kind: CommandKind.FILE,
action: mockAction,
};
mockGetCommands.mockReturnValue([mockCommand]);

View File

@@ -13,15 +13,56 @@ import {
type Config,
} from '@qwen-code/qwen-code-core';
import { CommandService } from './services/CommandService.js';
import { BuiltinCommandLoader } from './services/BuiltinCommandLoader.js';
import { FileCommandLoader } from './services/FileCommandLoader.js';
import type { CommandContext } from './ui/commands/types.js';
import {
CommandKind,
type CommandContext,
type SlashCommand,
} from './ui/commands/types.js';
import { createNonInteractiveUI } from './ui/noninteractive/nonInteractiveUi.js';
import type { LoadedSettings } from './config/settings.js';
import type { SessionStatsState } from './ui/contexts/SessionContext.js';
/**
* Filters commands based on the allowed built-in command names.
*
* - Always includes FILE commands
* - Only includes BUILT_IN commands if their name is in the allowed set
* - Excludes other command types (e.g., MCP_PROMPT) in non-interactive mode
*
* @param commands All loaded commands
* @param allowedBuiltinCommandNames Set of allowed built-in command names (empty = none allowed)
* @returns Filtered commands
*/
function filterCommandsForNonInteractive(
commands: readonly SlashCommand[],
allowedBuiltinCommandNames: Set<string>,
): SlashCommand[] {
return commands.filter((cmd) => {
if (cmd.kind === CommandKind.FILE) {
return true;
}
// Built-in commands: only include if in the allowed list
if (cmd.kind === CommandKind.BUILT_IN) {
return allowedBuiltinCommandNames.has(cmd.name);
}
// Exclude other types (e.g., MCP_PROMPT) in non-interactive mode
return false;
});
}
/**
* Processes a slash command in a non-interactive environment.
*
* @param rawQuery The raw query string (should start with '/')
* @param abortController Controller to cancel the operation
* @param config The configuration object
* @param settings The loaded settings
* @param allowedBuiltinCommandNames Optional array of built-in command names that are
* allowed. If not provided or empty, only file commands are available.
* @returns A Promise that resolves to `PartListUnion` if a valid command is
* found and results in a prompt, or `undefined` otherwise.
* @throws {FatalInputError} if the command result is not supported in
@@ -32,21 +73,35 @@ export const handleSlashCommand = async (
abortController: AbortController,
config: Config,
settings: LoadedSettings,
allowedBuiltinCommandNames?: string[],
): Promise<PartListUnion | undefined> => {
const trimmed = rawQuery.trim();
if (!trimmed.startsWith('/')) {
return;
}
// Only custom commands are supported for now.
const loaders = [new FileCommandLoader(config)];
const allowedBuiltinSet = new Set(allowedBuiltinCommandNames ?? []);
// Only load BuiltinCommandLoader if there are allowed built-in commands
const loaders =
allowedBuiltinSet.size > 0
? [new BuiltinCommandLoader(config), new FileCommandLoader(config)]
: [new FileCommandLoader(config)];
const commandService = await CommandService.create(
loaders,
abortController.signal,
);
const commands = commandService.getCommands();
const filteredCommands = filterCommandsForNonInteractive(
commands,
allowedBuiltinSet,
);
const { commandToExecute, args } = parseSlashCommand(rawQuery, commands);
const { commandToExecute, args } = parseSlashCommand(
rawQuery,
filteredCommands,
);
if (commandToExecute) {
if (commandToExecute.action) {
@@ -107,3 +162,44 @@ export const handleSlashCommand = async (
return;
};
/**
* Retrieves all available slash commands for the current configuration.
*
* @param config The configuration object
* @param settings The loaded settings
* @param abortSignal Signal to cancel the loading process
* @param allowedBuiltinCommandNames Optional array of built-in command names that are
* allowed. If not provided or empty, only file commands are available.
* @returns A Promise that resolves to an array of SlashCommand objects
*/
export const getAvailableCommands = async (
config: Config,
settings: LoadedSettings,
abortSignal: AbortSignal,
allowedBuiltinCommandNames?: string[],
): Promise<SlashCommand[]> => {
try {
const allowedBuiltinSet = new Set(allowedBuiltinCommandNames ?? []);
// Only load BuiltinCommandLoader if there are allowed built-in commands
const loaders =
allowedBuiltinSet.size > 0
? [new BuiltinCommandLoader(config), new FileCommandLoader(config)]
: [new FileCommandLoader(config)];
const commandService = await CommandService.create(loaders, abortSignal);
const commands = commandService.getCommands();
const filteredCommands = filterCommandsForNonInteractive(
commands,
allowedBuiltinSet,
);
// Filter out hidden commands
return filteredCommands.filter((cmd) => !cmd.hidden);
} catch (error) {
// Handle errors gracefully - log and return empty array
console.error('Error loading available commands:', error);
return [];
}
};

View File

@@ -78,20 +78,17 @@ export function AuthDialog({
);
const handleAuthSelect = (authMethod: AuthType) => {
const error = validateAuthMethod(authMethod);
if (error) {
if (
authMethod === AuthType.USE_OPENAI &&
!process.env['OPENAI_API_KEY']
) {
setShowOpenAIKeyPrompt(true);
setErrorMessage(null);
} else {
setErrorMessage(error);
}
} else {
if (authMethod === AuthType.USE_OPENAI) {
setShowOpenAIKeyPrompt(true);
setErrorMessage(null);
onSelect(authMethod, SettingScope.User);
} else {
const error = validateAuthMethod(authMethod);
if (error) {
setErrorMessage(error);
} else {
setErrorMessage(null);
onSelect(authMethod, SettingScope.User);
}
}
};
@@ -137,10 +134,23 @@ export function AuthDialog({
},
{ isActive: true },
);
const getDefaultOpenAIConfig = () => {
const fromSettings = settings.merged.security?.auth;
const modelSettings = settings.merged.model;
return {
apiKey: fromSettings?.apiKey || process.env['OPENAI_API_KEY'] || '',
baseUrl: fromSettings?.baseUrl || process.env['OPENAI_BASE_URL'] || '',
model: modelSettings?.name || process.env['OPENAI_MODEL'] || '',
};
};
if (showOpenAIKeyPrompt) {
const defaults = getDefaultOpenAIConfig();
return (
<OpenAIKeyPrompt
defaultApiKey={defaults.apiKey}
defaultBaseUrl={defaults.baseUrl}
defaultModel={defaults.model}
onSubmit={handleOpenAIKeySubmit}
onCancel={handleOpenAIKeyCancel}
/>

View File

@@ -17,7 +17,7 @@ import { terminalSetup } from '../utils/terminalSetup.js';
export const terminalSetupCommand: SlashCommand = {
name: 'terminal-setup',
description:
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf)',
'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)',
kind: CommandKind.BUILT_IN,
action: async (): Promise<MessageActionReturn> => {

View File

@@ -13,15 +13,21 @@ import { useKeypress } from '../hooks/useKeypress.js';
interface OpenAIKeyPromptProps {
onSubmit: (apiKey: string, baseUrl: string, model: string) => void;
onCancel: () => void;
defaultApiKey?: string;
defaultBaseUrl?: string;
defaultModel?: string;
}
export function OpenAIKeyPrompt({
onSubmit,
onCancel,
defaultApiKey,
defaultBaseUrl,
defaultModel,
}: OpenAIKeyPromptProps): React.JSX.Element {
const [apiKey, setApiKey] = useState('');
const [baseUrl, setBaseUrl] = useState('');
const [model, setModel] = useState('');
const [apiKey, setApiKey] = useState(defaultApiKey || '');
const [baseUrl, setBaseUrl] = useState(defaultBaseUrl || '');
const [model, setModel] = useState(defaultModel || '');
const [currentField, setCurrentField] = useState<
'apiKey' | 'baseUrl' | 'model'
>('apiKey');

View File

@@ -25,6 +25,7 @@ export const EDITOR_DISPLAY_NAMES: Record<EditorType, string> = {
vscodium: 'VSCodium',
windsurf: 'Windsurf',
zed: 'Zed',
trae: 'Trae',
};
class EditorSettingsManager {

View File

@@ -48,7 +48,7 @@ export interface TerminalSetupResult {
requiresRestart?: boolean;
}
type SupportedTerminal = 'vscode' | 'cursor' | 'windsurf';
type SupportedTerminal = 'vscode' | 'cursor' | 'windsurf' | 'trae';
// Terminal detection
async function detectTerminal(): Promise<SupportedTerminal | null> {
@@ -68,6 +68,11 @@ async function detectTerminal(): Promise<SupportedTerminal | null> {
) {
return 'windsurf';
}
if (process.env['TERM_PRODUCT']?.toLowerCase().includes('trae')) {
return 'trae';
}
// Check VS Code last since forks may also set VSCODE env vars
if (termProgram === 'vscode' || process.env['VSCODE_GIT_IPC_HANDLE']) {
return 'vscode';
@@ -86,6 +91,8 @@ async function detectTerminal(): Promise<SupportedTerminal | null> {
return 'cursor';
if (parentName.includes('code') || parentName.includes('Code'))
return 'vscode';
if (parentName.includes('trae') || parentName.includes('Trae'))
return 'trae';
} catch (error) {
// Continue detection even if process check fails
console.debug('Parent process detection failed:', error);
@@ -287,6 +294,10 @@ async function configureWindsurf(): Promise<TerminalSetupResult> {
return configureVSCodeStyle('Windsurf', 'Windsurf');
}
async function configureTrae(): Promise<TerminalSetupResult> {
return configureVSCodeStyle('Trae', 'Trae');
}
/**
* Main terminal setup function that detects and configures the current terminal.
*
@@ -333,6 +344,8 @@ export async function terminalSetup(): Promise<TerminalSetupResult> {
return configureCursor();
case 'windsurf':
return configureWindsurf();
case 'trae':
return configureTrae();
default:
return {
success: false,

View File

@@ -128,6 +128,14 @@ export type AgentRequest = z.infer<typeof agentRequestSchema>;
export type AgentNotification = z.infer<typeof agentNotificationSchema>;
export type AvailableCommandInput = z.infer<typeof availableCommandInputSchema>;
export type AvailableCommand = z.infer<typeof availableCommandSchema>;
export type AvailableCommandsUpdate = z.infer<
typeof availableCommandsUpdateSchema
>;
export const writeTextFileRequestSchema = z.object({
content: z.string(),
path: z.string(),
@@ -386,6 +394,21 @@ export const promptRequestSchema = z.object({
sessionId: z.string(),
});
export const availableCommandInputSchema = z.object({
hint: z.string(),
});
export const availableCommandSchema = z.object({
description: z.string(),
input: availableCommandInputSchema.nullable().optional(),
name: z.string(),
});
export const availableCommandsUpdateSchema = z.object({
availableCommands: z.array(availableCommandSchema),
sessionUpdate: z.literal('available_commands_update'),
});
export const sessionUpdateSchema = z.union([
z.object({
content: contentBlockSchema,
@@ -423,6 +446,7 @@ export const sessionUpdateSchema = z.union([
entries: z.array(planEntrySchema),
sessionUpdate: z.literal('plan'),
}),
availableCommandsUpdateSchema,
]);
export const agentResponseSchema = z.union([

View File

@@ -31,6 +31,7 @@ import {
MCPServerConfig,
ToolConfirmationOutcome,
logToolCall,
logUserPrompt,
getErrorStatus,
isWithinRoot,
isNodeError,
@@ -38,6 +39,7 @@ import {
TaskTool,
Kind,
TodoWriteTool,
UserPromptEvent,
} from '@qwen-code/qwen-code-core';
import * as acp from './acp.js';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -53,6 +55,26 @@ import { ExtensionStorage, type Extension } from '../config/extension.js';
import type { CliArgs } from '../config/config.js';
import { loadCliConfig } from '../config/config.js';
import { ExtensionEnablementManager } from '../config/extensions/extensionEnablement.js';
import {
handleSlashCommand,
getAvailableCommands,
} from '../nonInteractiveCliCommands.js';
import type { AvailableCommand, AvailableCommandsUpdate } from './schema.js';
import { isSlashCommand } from '../ui/utils/commandUtils.js';
/**
* Built-in commands that are allowed in ACP integration mode.
* Only these commands will be available when using handleSlashCommand
* or getAvailableCommands in ACP integration.
*
* Currently, only "init" is supported because `handleSlashCommand` in
* nonInteractiveCliCommands.ts only supports handling results where
* result.type is "submit_prompt". Other result types are either coupled
* to the UI or cannot send notifications to the client via ACP.
*
* If you have a good idea to add support for more commands, PRs are welcome!
*/
const ALLOWED_BUILTIN_COMMANDS_FOR_ACP = ['init'];
/**
* Resolves the model to use based on the current configuration.
@@ -151,7 +173,7 @@ class GeminiAgent {
cwd,
mcpServers,
}: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
const sessionId = randomUUID();
const sessionId = this.config.getSessionId() || randomUUID();
const config = await this.newSessionConfig(sessionId, cwd, mcpServers);
let isAuthenticated = false;
@@ -182,9 +204,20 @@ class GeminiAgent {
const geminiClient = config.getGeminiClient();
const chat = await geminiClient.startChat();
const session = new Session(sessionId, chat, config, this.client);
const session = new Session(
sessionId,
chat,
config,
this.client,
this.settings,
);
this.sessions.set(sessionId, session);
// Send available commands update as the first session update
setTimeout(async () => {
await session.sendAvailableCommandsUpdate();
}, 0);
return {
sessionId,
};
@@ -242,12 +275,14 @@ class GeminiAgent {
class Session {
private pendingPrompt: AbortController | null = null;
private turn: number = 0;
constructor(
private readonly id: string,
private readonly chat: GeminiChat,
private readonly config: Config,
private readonly client: acp.Client,
private readonly settings: LoadedSettings,
) {}
async cancelPendingPrompt(): Promise<void> {
@@ -264,10 +299,57 @@ class Session {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
const promptId = Math.random().toString(16).slice(2);
const chat = this.chat;
// Increment turn counter for each user prompt
this.turn += 1;
const parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
const chat = this.chat;
const promptId = this.config.getSessionId() + '########' + this.turn;
// Extract text from all text blocks to construct the full prompt text for logging
const promptText = params.prompt
.filter((block) => block.type === 'text')
.map((block) => (block.type === 'text' ? block.text : ''))
.join(' ');
// Log user prompt
logUserPrompt(
this.config,
new UserPromptEvent(
promptText.length,
promptId,
this.config.getContentGeneratorConfig()?.authType,
promptText,
),
);
// Check if the input contains a slash command
// Extract text from the first text block if present
const firstTextBlock = params.prompt.find((block) => block.type === 'text');
const inputText = firstTextBlock?.text || '';
let parts: Part[];
if (isSlashCommand(inputText)) {
// Handle slash command - allow specific built-in commands for ACP integration
const slashCommandResult = await handleSlashCommand(
inputText,
pendingSend,
this.config,
this.settings,
ALLOWED_BUILTIN_COMMANDS_FOR_ACP,
);
if (slashCommandResult) {
// Use the result from the slash command
parts = slashCommandResult as Part[];
} else {
// Slash command didn't return a prompt, continue with normal processing
parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
}
} else {
// Normal processing for non-slash commands
parts = await this.#resolvePrompt(params.prompt, pendingSend.signal);
}
let nextMessage: Content | null = { role: 'user', parts };
@@ -361,6 +443,37 @@ class Session {
await this.client.sessionUpdate(params);
}
async sendAvailableCommandsUpdate(): Promise<void> {
const abortController = new AbortController();
try {
const slashCommands = await getAvailableCommands(
this.config,
this.settings,
abortController.signal,
ALLOWED_BUILTIN_COMMANDS_FOR_ACP,
);
// Convert SlashCommand[] to AvailableCommand[] format for ACP protocol
const availableCommands: AvailableCommand[] = slashCommands.map(
(cmd) => ({
name: cmd.name,
description: cmd.description,
input: null,
}),
);
const update: AvailableCommandsUpdate = {
sessionUpdate: 'available_commands_update',
availableCommands,
};
await this.sendUpdate(update);
} catch (error) {
// Log error but don't fail session creation
console.error('Error sending available commands update:', error);
}
}
private async runTool(
abortSignal: AbortSignal,
promptId: string,

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.2.1",
"version": "0.2.2",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -23,6 +23,14 @@ import type OpenAI from 'openai';
import { safeJsonParse } from '../../utils/safeJsonParse.js';
import { StreamingToolCallParser } from './streamingToolCallParser.js';
/**
* Extended usage type that supports both OpenAI standard format and alternative formats
* Some models return cached_tokens at the top level instead of in prompt_tokens_details
*/
interface ExtendedCompletionUsage extends OpenAI.CompletionUsage {
cached_tokens?: number;
}
/**
* Tool call accumulator for streaming responses
*/
@@ -582,7 +590,13 @@ export class OpenAIContentConverter {
const promptTokens = usage.prompt_tokens || 0;
const completionTokens = usage.completion_tokens || 0;
const totalTokens = usage.total_tokens || 0;
const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
// Support both formats: prompt_tokens_details.cached_tokens (OpenAI standard)
// and cached_tokens (some models return it at top level)
const extendedUsage = usage as ExtendedCompletionUsage;
const cachedTokens =
usage.prompt_tokens_details?.cached_tokens ??
extendedUsage.cached_tokens ??
0;
// If we only have total tokens but no breakdown, estimate the split
// Typically input is ~70% and output is ~30% for most conversations
@@ -707,7 +721,13 @@ export class OpenAIContentConverter {
const promptTokens = usage.prompt_tokens || 0;
const completionTokens = usage.completion_tokens || 0;
const totalTokens = usage.total_tokens || 0;
const cachedTokens = usage.prompt_tokens_details?.cached_tokens || 0;
// Support both formats: prompt_tokens_details.cached_tokens (OpenAI standard)
// and cached_tokens (some models return it at top level)
const extendedUsage = usage as ExtendedCompletionUsage;
const cachedTokens =
usage.prompt_tokens_details?.cached_tokens ??
extendedUsage.cached_tokens ??
0;
// If we only have total tokens but no breakdown, estimate the split
// Typically input is ~70% and output is ~30% for most conversations

View File

@@ -72,6 +72,7 @@ describe('editor utils', () => {
{ editor: 'neovim', commands: ['nvim'], win32Commands: ['nvim'] },
{ editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] },
{ editor: 'emacs', commands: ['emacs'], win32Commands: ['emacs.exe'] },
{ editor: 'trae', commands: ['trae'], win32Commands: ['trae'] },
];
for (const { editor, commands, win32Commands } of testCases) {
@@ -171,6 +172,7 @@ describe('editor utils', () => {
},
{ editor: 'cursor', commands: ['cursor'], win32Commands: ['cursor'] },
{ editor: 'zed', commands: ['zed', 'zeditor'], win32Commands: ['zed'] },
{ editor: 'trae', commands: ['trae'], win32Commands: ['trae'] },
];
for (const { editor, commands, win32Commands } of guiEditors) {
@@ -321,6 +323,7 @@ describe('editor utils', () => {
'windsurf',
'cursor',
'zed',
'trae',
];
for (const editor of guiEditors) {
@@ -430,6 +433,7 @@ describe('editor utils', () => {
'windsurf',
'cursor',
'zed',
'trae',
];
for (const editor of guiEditors) {
it(`should not call onEditorClose for ${editor}`, async () => {
@@ -481,6 +485,7 @@ describe('editor utils', () => {
'windsurf',
'cursor',
'zed',
'trae',
];
for (const editor of guiEditors) {
it(`should not allow ${editor} in sandbox mode`, () => {

View File

@@ -14,7 +14,8 @@ export type EditorType =
| 'vim'
| 'neovim'
| 'zed'
| 'emacs';
| 'emacs'
| 'trae';
function isValidEditorType(editor: string): editor is EditorType {
return [
@@ -26,6 +27,7 @@ function isValidEditorType(editor: string): editor is EditorType {
'neovim',
'zed',
'emacs',
'trae',
].includes(editor);
}
@@ -62,6 +64,7 @@ const editorCommands: Record<
neovim: { win32: ['nvim'], default: ['nvim'] },
zed: { win32: ['zed'], default: ['zed', 'zeditor'] },
emacs: { win32: ['emacs.exe'], default: ['emacs'] },
trae: { win32: ['trae'], default: ['trae'] },
};
export function checkHasEditorType(editor: EditorType): boolean {
@@ -73,7 +76,9 @@ export function checkHasEditorType(editor: EditorType): boolean {
export function allowEditorTypeInSandbox(editor: EditorType): boolean {
const notUsingSandbox = !process.env['SANDBOX'];
if (['vscode', 'vscodium', 'windsurf', 'cursor', 'zed'].includes(editor)) {
if (
['vscode', 'vscodium', 'windsurf', 'cursor', 'zed', 'trae'].includes(editor)
) {
return notUsingSandbox;
}
// For terminal-based editors like vim and emacs, allow in sandbox.
@@ -115,6 +120,7 @@ export function getDiffCommand(
case 'windsurf':
case 'cursor':
case 'zed':
case 'trae':
return { command, args: ['--wait', '--diff', oldPath, newPath] };
case 'vim':
case 'neovim':

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.2.1",
"version": "0.2.2",
"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.2.1",
"version": "0.2.2",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {