mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-11 11:29:12 +00:00
Compare commits
6 Commits
v0.2.1
...
mingholy/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0afc50970f | ||
|
|
0eeffc6875 | ||
|
|
f0e21374c1 | ||
|
|
29261c75e1 | ||
|
|
b4eba6584a | ||
|
|
11ae56c18a |
12
package-lock.json
generated
12
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -25,6 +25,7 @@ export const EDITOR_DISPLAY_NAMES: Record<EditorType, string> = {
|
||||
vscodium: 'VSCodium',
|
||||
windsurf: 'Windsurf',
|
||||
zed: 'Zed',
|
||||
trae: 'Trae',
|
||||
};
|
||||
|
||||
class EditorSettingsManager {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`, () => {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user