Compare commits

..

5 Commits

Author SHA1 Message Date
yiliang114
c4e6c096dc feat(cli): improve LSP service implementation with type safety and iteration fixes
- Fix iteration over Map and Set collections by using Array.from() to avoid
  potential modification during iteration issues
- Add proper type casting for test mocks to ensure type safety
- Add null checks and type guards for LSP reference and symbol processing
- Improve type annotations for LSP server status and configuration objects
- Update path validation to use workspace root instead of config.cwd

These changes improve the robustness and type safety of the LSP service implementation.
2026-01-07 19:59:19 +08:00
yiliang114
4857f2f803 Merge branch 'feat/support-lsp-1' into feat/support-lsp 2026-01-07 15:22:34 +08:00
yiliang114
5a907c3415 wip(cli): support lsp 2026-01-07 15:21:33 +08:00
yiliang114
d1d215b82e wip(cli): support lsp 2026-01-05 10:18:24 +08:00
yiliang114
a67a8d0277 wip(cli): support lsp 2026-01-05 01:42:05 +08:00
134 changed files with 3206 additions and 11936 deletions

1
.gitignore vendored
View File

@@ -23,7 +23,6 @@ package-lock.json
.idea
*.iml
.cursor
.qoder
# OS metadata
.DS_Store

View File

@@ -13,5 +13,10 @@
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"vitest.disableWorkspaceWarning": true
"vitest.disableWorkspaceWarning": true,
"lsp": {
"enabled": true,
"allowed": ["typescript-language-server"],
"excluded": ["gopls"]
}
}

147
cclsp-integration-plan.md Normal file
View File

@@ -0,0 +1,147 @@
# Qwen Code CLI LSP 集成实现方案分析
## 1. 项目概述
本方案旨在将 LSPLanguage Server Protocol能力原生集成到 Qwen Code CLI 中,使 AI 代理能够利用代码导航、定义查找、引用查找等功能。LSP 将作为与 MCP 并行的一级扩展机制实现。
## 2. 技术方案对比
### 2.1 Piebald-AI/claude-code-lsps 方案
- **架构**: 客户端直接与每个 LSP 通信,通过 `.lsp.json` 配置文件声明服务器命令/参数、stdio 传输和文件扩展名路由
- **用户配置**: 低摩擦,只需放置 `.lsp.json` 配置并确保 LSP 二进制文件已安装
- **安全**: LSP 子进程以用户权限运行,无内置信任门控
- **功能覆盖**: 可以暴露完整的 LSP 表面hover、诊断、代码操作、重命名等
### 2.2 原生 LSP 客户端方案(推荐方案)
- **架构**: Qwen Code CLI 直接作为 LSP 客户端,与语言服务器建立 JSON-RPC 连接
- **用户配置**: 支持内置预设 + 用户自定义 `.lsp.json` 配置
- **安全**: 与 MCP 共享相同的安全控制(信任工作区、允许/拒绝列表、确认提示)
- **功能覆盖**: 暴露完整的 LSP 功能(流式诊断、代码操作、重命名、语义标记等)
### 2.3 cclsp + MCP 方案(备选)
- **架构**: 通过 MCP 协议调用 cclsp 作为 LSP 桥接
- **用户配置**: 需要 MCP 配置
- **安全**: 通过 MCP 安全控制
- **功能覆盖**: 依赖于 cclsp 映射的 MCP 工具
## 3. 原生 LSP 集成详细计划
### 3.1 方案选择
- **推荐方案**: 原生 LSP 客户端作为主要路径,因为它提供完整 LSP 功能、更低延迟和更好的用户体验
- **兼容层**: 保留 cclsp+MCP 作为现有 MCP 工作流的兼容桥接
- **并行架构**: LSP 和 MCP 作为独立的扩展机制共存,共享安全策略
### 3.2 实现步骤
#### 3.2.1 创建原生 LSP 服务
`packages/cli/src/services/lsp/` 目录下创建 `NativeLspService` 类,处理:
- 工作区语言检测
- 自动发现和启动语言服务器
- 与现有文档/编辑模型同步
- LSP 能力直接暴露给代理
#### 3.2.2 配置支持
- 支持内置预设配置(常见语言服务器)
- 支持用户自定义 `.lsp.json` 配置文件
- 与 MCP 配置共存,共享信任控制
#### 3.2.3 集成启动流程
-`packages/cli/src/config/config.ts` 中的 `loadCliConfig` 函数内集成
- 确保 LSP 服务与 MCP 服务共享相同的安全控制机制
- 处理沙箱预检和主运行的重复调用问题
#### 3.2.4 功能标志配置
-`packages/cli/src/config/settingsSchema.ts` 中添加新的设置项
- 提供全局开关(如 `lsp.enabled=false`)允许用户禁用 LSP 功能
- 尊重 `mcp.allowed`/`mcp.excluded` 和文件夹信任设置
#### 3.2.5 安全控制
- 与 MCP 共享相同的安全控制机制
- 在信任工作区中自动启用,在非信任工作区中提示用户
- 实现路径允许列表和进程启动确认
#### 3.2.6 错误处理与用户通知
- 检测缺失的语言服务器并提供安装命令
- 通过现有 MCP 状态 UI 显示错误信息
- 实现重试/退避机制,检测沙箱环境并抑制自动启动
### 3.3 需要确认的不确定项
1. **启动集成点**:在 `loadCliConfig` 中集成原生 LSP 服务,需确保与 MCP 服务的协调
2. **配置优先级**:如果用户已有 cclsp MCP 配置,应保持并存还是优先使用原生 LSP
3. **功能开关设计**开关应该是全局级别的LSP 和 MCP 可独立启用/禁用
4. **共享安全模型**:如何在代码中复用 MCP 的信任/安全控制逻辑
5. **语言服务器管理**:如何管理 LSP 服务器生命周期并与文档编辑模型同步
6. **依赖检测机制**:检测 LSP 服务器可用性,失败时提供降级选项
7. **测试策略**:需要测试 LSP 与 MCP 的并行运行,以及共享安全控制
### 3.4 安全考虑
- 与 MCP 共享相同的安全控制模型
- 仅在受信任工作区中启用自动 LSP 功能
- 提供用户确认机制用于启动新的 LSP 服务器
- 防止路径劫持,使用安全的路径解析
### 3.5 高级 LSP 功能支持
- **完整 LSP 功能**: 支持流式诊断、代码操作、重命名、语义高亮、工作区编辑等
- **兼容 Claude 配置**: 支持导入 Claude Code 风格的 `.lsp.json` 配置
- **性能优化**: 优化 LSP 服务器启动时间和内存使用
### 3.6 用户体验
- 提供安装提示而非自动安装
- 在统一的状态界面显示 LSP 和 MCP 服务器状态
- 提供独立开关让用户控制 LSP 和 MCP 功能
- 为只读/沙箱环境提供安全的配置处理和清晰的错误消息
## 4. 实施总结
### 4.1 已完成的工作
1. **NativeLspService 类**创建了核心服务类包含语言检测、配置合并、LSP 连接管理等功能
2. **LSP 连接工厂**:实现了基于 stdio 的 LSP 连接创建和管理
3. **语言检测机制**:实现了基于文件扩展名和项目配置文件的语言自动检测
4. **配置系统**:实现了内置预设、用户配置和 Claude 兼容配置的合并
5. **安全控制**:实现了与 MCP 共享的安全控制机制,包括信任检查、用户确认、路径安全验证
6. **CLI 集成**:在 `loadCliConfig` 函数中集成了 LSP 服务初始化点
### 4.2 关键组件
#### 4.2.1 LspConnectionFactory
- 使用 `vscode-jsonrpc``vscode-languageserver-protocol` 实现 LSP 连接
- 支持 stdio 传输方式,可以扩展支持 TCP 传输
- 提供连接创建、初始化和关闭的完整生命周期管理
#### 4.2.2 NativeLspService
- **语言检测**:扫描项目文件和配置文件来识别编程语言
- **配置合并**:按优先级合并内置预设、用户配置和兼容层配置
- **LSP 服务器管理**:启动、停止和状态管理
- **安全控制**:与 MCP 共享的信任和确认机制
#### 4.2.3 配置架构
- **内置预设**:为常见语言提供默认 LSP 服务器配置
- **用户配置**:支持 `.lsp.json` 文件格式
- **Claude 兼容**:可导入 Claude Code 的 LSP 配置
### 4.3 依赖管理
- 使用 `vscode-languageserver-protocol` 进行 LSP 协议通信
- 使用 `vscode-jsonrpc` 进行 JSON-RPC 消息传递
- 使用 `vscode-languageserver-textdocument` 管理文档版本
### 4.4 安全特性
- 工作区信任检查
- 用户确认机制(对于非信任工作区)
- 命令存在性验证
- 路径安全性检查
## 5. 总结
原生 LSP 客户端是当前最符合 Qwen Code 架构的选择,它提供了完整的 LSP 功能、更低的延迟和更好的用户体验。LSP 作为与 MCP 并行的一级扩展机制,将与 MCP 共享安全控制策略但提供更丰富的代码智能功能。cclsp+MCP 可作为兼容层保留,以支持现有的 MCP 工作流。
该实现方案将使 Qwen Code CLI 具备完整的 LSP 功能,包括代码跳转、引用查找、自动补全、代码诊断等,为 AI 代理提供更丰富的代码理解能力。

View File

@@ -11,7 +11,6 @@ export default {
type: 'separator',
},
'sdk-typescript': 'Typescript SDK',
'sdk-java': 'Java SDK(alpha)',
'Dive Into Qwen Code': {
title: 'Dive Into Qwen Code',
type: 'separator',

View File

@@ -1,312 +0,0 @@
# Qwen Code Java SDK
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
## Requirements
- Java >= 1.8
- Maven >= 3.6.0 (for building from source)
- qwen-code >= 0.5.0
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Installation
Add the following dependency to your Maven `pom.xml`:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>{$version}</version>
</dependency>
```
Or if using Gradle, add to your `build.gradle`:
```gradle
implementation 'com.alibaba:qwencode-sdk:{$version}'
```
## Building and Running
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
```
## Quick Start
The simplest way to use the SDK is through the `QwenCodeCli.simpleQuery()` method:
```java
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
```
For more advanced usage with custom transport options:
```java
public static void runTransportOptionsExample() {
TransportOptions options = new TransportOptions()
.setModel("qwen3-coder-flash")
.setPermissionMode(PermissionMode.AUTO_EDIT)
.setCwd("./")
.setEnv(new HashMap<String, String>() {{put("CUSTOM_VAR", "value");}})
.setIncludePartialMessages(true)
.setTurnTimeout(new Timeout(120L, TimeUnit.SECONDS))
.setMessageTimeout(new Timeout(90L, TimeUnit.SECONDS))
.setAllowedTools(Arrays.asList("read_file", "write_file", "list_directory"));
List<String> result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options);
result.forEach(logger::info);
}
```
For streaming content handling with custom content consumers:
```java
public static void runStreamingExample() {
QwenCodeCli.simpleQuery("who are you, what are your capabilities?",
new TransportOptions().setMessageTimeout(new Timeout(10L, TimeUnit.SECONDS)), new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
logger.info("Text content received: {}", textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
logger.info("Thinking content received: {}", thingkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseContent) {
logger.info("Tool use content received: {} with arguments: {}",
toolUseContent, toolUseContent.getInput());
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultContent) {
logger.info("Tool result content received: {}", toolResultContent.getContent());
}
@Override
public void onOtherContent(Session session, AssistantContent<?> other) {
logger.info("Other content received: {}", other);
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
logger.info("Usage information received: Input tokens: {}, Output tokens: {}",
assistantUsage.getUsage().getInputTokens(), assistantUsage.getUsage().getOutputTokens());
}
}.setDefaultPermissionOperation(Operation.allow));
logger.info("Streaming example completed.");
}
```
other examples see src/test/java/com/alibaba/qwen/code/cli/example
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Features
### Permission Modes
The SDK supports different permission modes for controlling tool execution:
- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.
### Session Event Consumers and Assistant Content Consumers
The SDK provides two key interfaces for handling events and content from the CLI:
#### SessionEventConsumers Interface
The `SessionEventConsumers` interface provides callbacks for different types of messages during a session:
- `onSystemMessage`: Handles system messages from the CLI (receives Session and SDKSystemMessage)
- `onResultMessage`: Handles result messages from the CLI (receives Session and SDKResultMessage)
- `onAssistantMessage`: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)
- `onPartialAssistantMessage`: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)
- `onUserMessage`: Handles user messages (receives Session and SDKUserMessage)
- `onOtherMessage`: Handles other types of messages (receives Session and String message)
- `onControlResponse`: Handles control responses (receives Session and CLIControlResponse)
- `onControlRequest`: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)
#### AssistantContentConsumers Interface
The `AssistantContentConsumers` interface handles different types of content within assistant messages:
- `onText`: Handles text content (receives Session and TextAssistantContent)
- `onThinking`: Handles thinking content (receives Session and ThingkingAssistantContent)
- `onToolUse`: Handles tool use content (receives Session and ToolUseAssistantContent)
- `onToolResult`: Handles tool result content (receives Session and ToolResultAssistantContent)
- `onOtherContent`: Handles other content types (receives Session and AssistantContent)
- `onUsage`: Handles usage information (receives Session and AssistantUsage)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)
- `onOtherControlRequest`: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)
#### Relationship Between the Interfaces
**Important Note on Event Hierarchy:**
- `SessionEventConsumers` is the **high-level** event processor that handles different message types (system, assistant, user, etc.)
- `AssistantContentConsumers` is the **low-level** content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)
**Processor Relationship:**
- `SessionEventConsumers``AssistantContentConsumers` (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)
**Event Derivation Relationships:**
- `onAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``onPermissionRequest`, `onOtherControlRequest`
**Event Timeout Relationships:**
Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:
- `onSystemMessage``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default Timeout Values:**
- `SessionEventSimpleConsumers` default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)
- `AssistantContentSimpleConsumers` default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)
**Timeout Hierarchy Requirements:**
For proper operation, the following timeout relationships should be maintained:
- `onAssistantMessageTimeout` return value should be greater than `onTextTimeout`, `onThinkingTimeout`, `onToolUseTimeout`, `onToolResultTimeout`, and `onOtherContentTimeout` return values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values
### Transport Options
The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:
- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: Additional command-line options to pass to the CLI
### Session Control Features
- **Session creation**: Use `QwenCodeCli.newSession()` to create a new session with custom options
- **Session management**: The `Session` class provides methods to send prompts, handle responses, and manage session state
- **Session cleanup**: Always close sessions using `session.close()` to properly terminate the CLI process
- **Session resumption**: Use `setResumeSessionId()` in `TransportOptions` to resume a previous session
- **Session interruption**: Use `session.interrupt()` to interrupt a currently running prompt
- **Dynamic model switching**: Use `session.setModel()` to change the model during a session
- **Dynamic permission mode switching**: Use `session.setPermissionMode()` to change the permission mode during a session
### Thread Pool Configuration
The SDK uses a thread pool for managing concurrent operations with the following default configuration:
- **Core Pool Size**: 30 threads
- **Maximum Pool Size**: 100 threads
- **Keep-Alive Time**: 60 seconds
- **Queue Capacity**: 300 tasks (using LinkedBlockingQueue)
- **Thread Naming**: "qwen_code_cli-pool-{number}"
- **Daemon Threads**: false
- **Rejected Execution Handler**: CallerRunsPolicy
## Error Handling
The SDK provides specific exception types for different error scenarios:
- `SessionControlException`: Thrown when there's an issue with session control (creation, initialization, etc.)
- `SessionSendPromptException`: Thrown when there's an issue sending a prompt or receiving a response
- `SessionClosedException`: Thrown when attempting to use a closed session
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: yes, requires Qwen CLI 0.5.5 or higher.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### Q: How do I handle long-running requests?
A: The SDK includes timeout utilities. You can configure timeouts using the `Timeout` class in `TransportOptions`.
### Q: Why are some tools not executing?
A: This is likely due to permission modes. Check your permission mode settings and consider using `allowedTools` to pre-approve certain tools.
### Q: How do I resume a previous session?
A: Use the `setResumeSessionId()` method in `TransportOptions` to resume a previous session.
### Q: Can I customize the environment for the CLI process?
A: Yes, use the `setEnv()` method in `TransportOptions` to pass environment variables to the CLI process.
## License
Apache-2.0 - see [LICENSE](./LICENSE) for details.

View File

@@ -381,7 +381,7 @@ Arguments passed directly when running the CLI can override other configurations
| `--telemetry-otlp-protocol` | | Sets the OTLP protocol for telemetry (`grpc` or `http`). | | Defaults to `grpc`. See [telemetry](../../developers/development/telemetry) for more information. |
| `--telemetry-log-prompts` | | Enables logging of prompts for telemetry. | | See [telemetry](../../developers/development/telemetry) for more information. |
| `--checkpointing` | | Enables [checkpointing](../features/checkpointing). | | |
| `--acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
| `--experimental-acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Experimental. |
| `--experimental-skills` | | Enables experimental [Agent Skills](../features/skills) (registers the `skill` tool and loads Skills from `.qwen/skills/` and `~/.qwen/skills/`). | | Experimental. |
| `--extensions` | `-e` | Specifies a list of extensions to use for the session. | Extension names | If not provided, all available extensions are used. Use the special term `qwen -e none` to disable all extensions. Example: `qwen -e my-extension -e my-other-extension` |
| `--list-extensions` | `-l` | Lists all available extensions and exits. | | |

View File

@@ -32,7 +32,7 @@
"Qwen Code": {
"type": "custom",
"command": "qwen",
"args": ["--acp"],
"args": ["--experimental-acp"],
"env": {}
}
```

View File

@@ -80,11 +80,10 @@ type PermissionHandler = (
/**
* Sets up an ACP test environment with all necessary utilities.
* @param useNewFlag - If true, uses --acp; if false, uses --experimental-acp (for backward compatibility testing)
*/
function setupAcpTest(
rig: TestRig,
options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean },
options?: { permissionHandler?: PermissionHandler },
) {
const pending = new Map<number, PendingRequest>();
let nextRequestId = 1;
@@ -96,13 +95,9 @@ function setupAcpTest(
const permissionHandler =
options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' }));
// Use --acp by default, but allow testing with --experimental-acp for backward compatibility
const acpFlag =
options?.useNewFlag !== false ? '--acp' : '--experimental-acp';
const agent = spawn(
'node',
[rig.bundlePath, acpFlag, '--no-chat-recording'],
[rig.bundlePath, '--experimental-acp', '--no-chat-recording'],
{
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -626,99 +621,3 @@ function setupAcpTest(
}
});
});
(IS_SANDBOX ? describe.skip : describe)(
'acp flag backward compatibility',
() => {
it('should work with deprecated --experimental-acp flag and show warning', async () => {
const rig = new TestRig();
rig.setup('acp backward compatibility');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: false,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).toContain('--experimental-acp is deprecated');
expect(stderrOutput).toContain('Please use --acp instead');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality still works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('should work with new --acp flag without warnings', async () => {
const rig = new TestRig();
rig.setup('acp new flag');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig, {
useNewFlag: true,
});
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// Verify no deprecation warning is shown
const stderrOutput = stderr.join('');
expect(stderrOutput).not.toContain('--experimental-acp is deprecated');
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Verify functionality works
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: 'Say hello.' }],
});
expect(promptResult).toBeDefined();
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
},
);

20
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.6.1",
"version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.6.1",
"version": "0.6.0",
"workspaces": [
"packages/*"
],
@@ -39,6 +39,7 @@
"globals": "^16.0.0",
"husky": "^9.1.7",
"json": "^11.0.0",
"json-schema": "^0.4.0",
"lint-staged": "^16.1.6",
"memfs": "^4.42.0",
"mnemonist": "^0.40.3",
@@ -10807,6 +10808,13 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true,
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -17316,7 +17324,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.6.1",
"version": "0.6.0",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17953,7 +17961,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.6.1",
"version": "0.6.0",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21413,7 +21421,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.6.1",
"version": "0.6.0",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21425,7 +21433,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.6.1",
"version": "0.6.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.6.1",
"version": "0.6.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.6.1"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
},
"scripts": {
"start": "cross-env node scripts/start.js",
@@ -94,6 +94,7 @@
"globals": "^16.0.0",
"husky": "^9.1.7",
"json": "^11.0.0",
"json-schema": "^0.4.0",
"lint-staged": "^16.1.6",
"memfs": "^4.42.0",
"mnemonist": "^0.40.3",

View File

@@ -0,0 +1,107 @@
# LSP 调试指南
本指南介绍如何调试 packages/cli 中的 LSP (Language Server Protocol) 功能。
## 1. 启用调试模式
CLI 支持调试模式,可以提供额外的日志信息:
```bash
# 使用 debug 标志运行
qwen --debug [你的命令]
# 或设置环境变量
DEBUG=true qwen [你的命令]
DEBUG_MODE=true qwen [你的命令]
```
## 2. LSP 配置选项
LSP 功能通过设置系统配置,包含以下选项:
- `lsp.enabled`: 启用/禁用原生 LSP 客户端(默认为 `false`
- `lsp.allowed`: 允许的 LSP 服务器名称白名单
- `lsp.excluded`: 排除的 LSP 服务器名称黑名单
在 settings.json 中的示例配置:
```json
{
"lsp": {
"enabled": true,
"allowed": ["typescript-language-server", "pylsp"],
"excluded": ["gopls"]
}
}
```
## 3. NativeLspService 调试功能
`NativeLspService` 类包含几个调试功能:
### 3.1 控制台日志
服务向控制台输出状态消息:
- `LSP 服务器 ${name} 启动成功` - 服务器成功启动
- `LSP 服务器 ${name} 启动失败` - 服务器启动失败
- `工作区不受信任,跳过 LSP 服务器发现` - 工作区不受信任,跳过发现
### 3.2 错误处理
服务具有全面的错误处理和详细的错误消息
### 3.3 状态跟踪
您可以通过 `getStatus()` 方法检查所有 LSP 服务器的状态
## 4. 调试命令
```bash
# 启用调试运行
qwen --debug --prompt "调试 LSP 功能"
# 检查在您的项目中检测到哪些 LSP 服务器
# 系统会自动检测语言和相应的 LSP 服务器
```
## 5. 手动 LSP 服务器配置
您还可以在项目根目录使用 `.lsp.json` 文件手动配置 LSP 服务器:
```json
{
"python": {
"command": "pylsp",
"args": [],
"transport": "stdio",
"trustRequired": true
}
}
```
## 6. LSP 问题排查
### 6.1 检查 LSP 服务器是否已安装
- 对于 TypeScript/JavaScript: `typescript-language-server`
- 对于 Python: `pylsp`
- 对于 Go: `gopls`
### 6.2 验证工作区信任
- LSP 服务器可能需要受信任的工作区才能启动
- 检查 `security.folderTrust.enabled` 设置
### 6.3 查看日志
- 查找以 `LSP 服务器` 开头的控制台消息
- 检查命令存在性和路径安全性问题
## 7. LSP 服务启动流程
LSP 服务的启动遵循以下流程:
1. **发现和准备**: `discoverAndPrepare()` 方法检测工作区中的编程语言
2. **创建服务器句柄**: 根据检测到的语言创建对应的服务器句柄
3. **启动服务器**: `start()` 方法启动所有服务器句柄
4. **状态管理**: 服务器状态在 `NOT_STARTED`, `IN_PROGRESS`, `READY`, `FAILED` 之间转换
## 8. 调试技巧
- 使用 `--debug` 标志查看详细的启动过程
- 检查工作区是否受信任(影响 LSP 服务器启动)
- 确认 LSP 服务器命令在系统 PATH 中可用
- 使用 `getStatus()` 方法监控服务器运行状态

View File

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

View File

@@ -21,6 +21,23 @@ import * as ServerConfig from '@qwen-code/qwen-code-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
const mockDiscoverAndPrepare = vi.fn();
const mockStartLsp = vi.fn();
const mockDefinitions = vi.fn().mockResolvedValue([]);
const mockReferences = vi.fn().mockResolvedValue([]);
const mockWorkspaceSymbols = vi.fn().mockResolvedValue([]);
const nativeLspServiceMock = vi.fn().mockImplementation(() => ({
discoverAndPrepare: mockDiscoverAndPrepare,
start: mockStartLsp,
definitions: mockDefinitions,
references: mockReferences,
workspaceSymbols: mockWorkspaceSymbols,
}));
vi.mock('../services/lsp/NativeLspService.js', () => ({
NativeLspService: nativeLspServiceMock,
}));
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi
.fn()
@@ -518,6 +535,16 @@ describe('loadCliConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
mockDiscoverAndPrepare.mockReset();
mockStartLsp.mockReset();
mockWorkspaceSymbols.mockReset();
mockWorkspaceSymbols.mockResolvedValue([]);
nativeLspServiceMock.mockReset();
nativeLspServiceMock.mockImplementation(() => ({
discoverAndPrepare: mockDiscoverAndPrepare,
start: mockStartLsp,
workspaceSymbols: mockWorkspaceSymbols,
}));
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});
@@ -587,6 +614,61 @@ describe('loadCliConfig', () => {
expect(config.getShowMemoryUsage()).toBe(false);
});
it('should initialize native LSP service when enabled', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
lsp: {
enabled: true,
allowed: ['typescript-language-server'],
excluded: ['pylsp'],
},
};
const config = await loadCliConfig(
settings,
[],
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
);
expect(config.isLspEnabled()).toBe(true);
expect(config.getLspAllowed()).toEqual(['typescript-language-server']);
expect(config.getLspExcluded()).toEqual(['pylsp']);
expect(nativeLspServiceMock).toHaveBeenCalledTimes(1);
expect(mockDiscoverAndPrepare).toHaveBeenCalledTimes(1);
expect(mockStartLsp).toHaveBeenCalledTimes(1);
const options = nativeLspServiceMock.mock.calls[0][5];
expect(options?.allowedServers).toEqual(['typescript-language-server']);
expect(options?.excludedServers).toEqual(['pylsp']);
});
it('should skip native LSP startup when startLsp option is false', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
const settings: Settings = { lsp: { enabled: true } };
const config = await loadCliConfig(
settings,
[],
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
undefined,
{ startLsp: false },
);
expect(config.isLspEnabled()).toBe(true);
expect(nativeLspServiceMock).not.toHaveBeenCalled();
expect(mockDiscoverAndPrepare).not.toHaveBeenCalled();
});
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments({} as Settings);
@@ -1597,58 +1679,6 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should not exclude a tool explicitly allowed in tools.allowed', async () => {
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
tools: {
allowed: [ShellTool.Name],
},
};
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
extensions,
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(ShellTool.Name);
expect(excludedTools).toContain(EditTool.Name);
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should not exclude a tool explicitly allowed in tools.core', async () => {
process.argv = ['node', 'script.js', '-p', 'test'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
tools: {
core: [ShellTool.Name],
},
};
const extensions: Extension[] = [];
const config = await loadCliConfig(
settings,
extensions,
new ExtensionEnablementManager(
ExtensionStorage.getUserExtensionsDir(),
argv.extensions,
),
argv,
);
const excludedTools = config.getExcludeTools();
expect(excludedTools).not.toContain(ShellTool.Name);
expect(excludedTools).toContain(EditTool.Name);
expect(excludedTools).toContain(WriteFileTool.Name);
});
it('should exclude only shell tools in non-interactive mode with auto-edit approval mode', async () => {
process.argv = [
'node',

View File

@@ -10,24 +10,24 @@ import {
Config,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
FileDiscoveryService,
getCurrentGeminiMdFilename,
loadServerHierarchicalMemory,
setGeminiMdFilename as setServerGeminiMdFilename,
ShellTool,
WriteFileTool,
resolveTelemetrySettings,
FatalConfigError,
Storage,
InputFormat,
OutputFormat,
isToolEnabled,
SessionService,
ideContextStore,
type ResumedSessionData,
type FileFilteringOptions,
type MCPServerConfig,
type ToolName,
EditTool,
ShellTool,
WriteFileTool,
type LspClient,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import type { Settings } from './settings.js';
@@ -44,6 +44,7 @@ import { annotateActiveExtensions } from './extension.js';
import { loadSandboxConfig } from './sandboxConfig.js';
import { appEvents } from '../utils/events.js';
import { mcpCommand } from '../commands/mcp.js';
import { NativeLspService } from '../services/lsp/NativeLspService.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
@@ -113,7 +114,6 @@ export interface CliArgs {
telemetryOutfile: string | undefined;
allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined;
acp: boolean | undefined;
experimentalAcp: boolean | undefined;
experimentalSkills: boolean | undefined;
extensions: string[] | undefined;
@@ -150,6 +150,44 @@ export interface CliArgs {
channel: string | undefined;
}
export interface LoadCliConfigOptions {
/**
* Whether to start the native LSP service during config load.
* Disable when doing preflight runs (e.g., sandbox preparation).
*/
startLsp?: boolean;
}
class NativeLspClient implements LspClient {
constructor(private readonly service: NativeLspService) {}
workspaceSymbols(query: string, limit?: number) {
return this.service.workspaceSymbols(query, limit);
}
definitions(
location: Parameters<NativeLspService['definitions']>[0],
serverName?: string,
limit?: number,
) {
return this.service.definitions(location, serverName, limit);
}
references(
location: Parameters<NativeLspService['references']>[0],
serverName?: string,
includeDeclaration?: boolean,
limit?: number,
) {
return this.service.references(
location,
serverName,
includeDeclaration,
limit,
);
}
}
function normalizeOutputFormat(
format: string | OutputFormat | undefined,
): OutputFormat | undefined {
@@ -307,15 +345,9 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
description: 'Enables checkpointing of file edits',
default: false,
})
.option('acp', {
type: 'boolean',
description: 'Starts the agent in ACP mode',
})
.option('experimental-acp', {
type: 'boolean',
description:
'Starts the agent in ACP mode (deprecated, use --acp instead)',
hidden: true,
description: 'Starts the agent in ACP mode',
})
.option('experimental-skills', {
type: 'boolean',
@@ -598,19 +630,8 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
// Handle deprecated --experimental-acp flag
if (result['experimentalAcp']) {
console.warn(
'\x1b[33m⚠ Warning: --experimental-acp is deprecated and will be removed in a future release. Please use --acp instead.\x1b[0m',
);
// Map experimental-acp to acp if acp is not explicitly set
if (!result['acp']) {
(result as Record<string, unknown>)['acp'] = true;
}
}
// Apply ACP fallback: if acp or experimental-acp is present but no explicit --channel, treat as ACP
if ((result['acp'] || result['experimentalAcp']) && !result['channel']) {
// Apply ACP fallback: if experimental-acp is present but no explicit --channel, treat as ACP
if (result['experimentalAcp'] && !result['channel']) {
(result as Record<string, unknown>)['channel'] = 'ACP';
}
@@ -675,6 +696,7 @@ export async function loadCliConfig(
extensionEnablementManager: ExtensionEnablementManager,
argv: CliArgs,
cwd: string = process.cwd(),
options: LoadCliConfigOptions = {},
): Promise<Config> {
const debugMode = isDebugMode(argv);
@@ -751,6 +773,12 @@ export async function loadCliConfig(
);
let mcpServers = mergeMcpServers(settings, activeExtensions);
// LSP configuration derived from settings; defaults to disabled for safety.
const lspEnabled = settings.lsp?.enabled ?? false;
const lspAllowed = settings.lsp?.allowed ?? settings.mcp?.allowed;
const lspExcluded = settings.lsp?.excluded ?? settings.mcp?.excluded;
let lspClient: LspClient | undefined;
const question = argv.promptInteractive || argv.prompt || '';
const inputFormat: InputFormat =
(argv.inputFormat as InputFormat | undefined) ?? InputFormat.TEXT;
@@ -838,28 +866,6 @@ export async function loadCliConfig(
// However, if stream-json input is used, control can be requested via JSON messages,
// so tools should not be excluded in that case.
const extraExcludes: string[] = [];
const resolvedCoreTools = argv.coreTools || settings.tools?.core || [];
const resolvedAllowedTools =
argv.allowedTools || settings.tools?.allowed || [];
const isExplicitlyEnabled = (toolName: ToolName): boolean => {
if (resolvedCoreTools.length > 0) {
if (isToolEnabled(toolName, resolvedCoreTools, [])) {
return true;
}
}
if (resolvedAllowedTools.length > 0) {
if (isToolEnabled(toolName, resolvedAllowedTools, [])) {
return true;
}
}
return false;
};
const excludeUnlessExplicit = (toolName: ToolName): void => {
if (!isExplicitlyEnabled(toolName)) {
extraExcludes.push(toolName);
}
};
if (
!interactive &&
!argv.experimentalAcp &&
@@ -868,15 +874,12 @@ export async function loadCliConfig(
switch (approvalMode) {
case ApprovalMode.PLAN:
case ApprovalMode.DEFAULT:
// In default non-interactive mode, all tools that require approval are excluded,
// unless explicitly enabled via coreTools/allowedTools.
excludeUnlessExplicit(ShellTool.Name as ToolName);
excludeUnlessExplicit(EditTool.Name as ToolName);
excludeUnlessExplicit(WriteFileTool.Name as ToolName);
// In default non-interactive mode, all tools that require approval are excluded.
extraExcludes.push(ShellTool.Name, EditTool.Name, WriteFileTool.Name);
break;
case ApprovalMode.AUTO_EDIT:
// In auto-edit non-interactive mode, only tools that still require a prompt are excluded.
excludeUnlessExplicit(ShellTool.Name as ToolName);
extraExcludes.push(ShellTool.Name);
break;
case ApprovalMode.YOLO:
// No extra excludes for YOLO mode.
@@ -979,7 +982,7 @@ export async function loadCliConfig(
}
}
return new Config({
const config = new Config({
sessionId,
sessionData,
embeddingModel: DEFAULT_QWEN_EMBEDDING_MODEL,
@@ -1026,7 +1029,7 @@ export async function loadCliConfig(
sessionTokenLimit: settings.model?.sessionTokenLimit ?? -1,
maxSessionTurns:
argv.maxSessionTurns ?? settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
experimentalZedIntegration: argv.experimentalAcp || false,
experimentalSkills: argv.experimentalSkills || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,
@@ -1082,7 +1085,39 @@ export async function loadCliConfig(
// always be true and the settings file can never disable recording.
chatRecording:
argv.chatRecording ?? settings.general?.chatRecording ?? true,
lsp: {
enabled: lspEnabled,
allowed: lspAllowed,
excluded: lspExcluded,
},
});
const shouldStartLsp = options.startLsp ?? true;
if (shouldStartLsp && lspEnabled) {
try {
const lspService = new NativeLspService(
config,
config.getWorkspaceContext(),
appEvents,
fileService,
ideContextStore,
{
allowedServers: lspAllowed,
excludedServers: lspExcluded,
requireTrustedWorkspace: folderTrust,
},
);
await lspService.discoverAndPrepare();
await lspService.start();
lspClient = new NativeLspClient(lspService);
config.setLspClient(lspClient);
} catch (err) {
logger.warn('Failed to initialize native LSP service:', err);
}
}
return config;
}
function allowedMcpServers(

View File

@@ -0,0 +1,38 @@
import type { JSONSchema7 } from 'json-schema';
export const lspSettingsSchema: JSONSchema7 = {
type: 'object',
properties: {
'lsp.enabled': {
type: 'boolean',
default: true,
description: '启用 LSP 语言服务器协议支持'
},
'lsp.allowed': {
type: 'array',
items: {
type: 'string'
},
default: [],
description: '允许运行的 LSP 服务器列表'
},
'lsp.excluded': {
type: 'array',
items: {
type: 'string'
},
default: [],
description: '禁止运行的 LSP 服务器列表'
},
'lsp.autoDetect': {
type: 'boolean',
default: true,
description: '自动检测项目语言并启动相应 LSP 服务器'
},
'lsp.serverTimeout': {
type: 'number',
default: 10000,
description: 'LSP 服务器启动超时时间(毫秒)'
}
}
};

View File

@@ -160,6 +160,34 @@ export function getSystemDefaultsPath(): string {
);
}
function getVsCodeSettingsPath(workspaceDir: string): string {
return path.join(workspaceDir, '.vscode', 'settings.json');
}
function loadVsCodeSettings(workspaceDir: string): Settings {
const vscodeSettingsPath = getVsCodeSettingsPath(workspaceDir);
try {
if (fs.existsSync(vscodeSettingsPath)) {
const content = fs.readFileSync(vscodeSettingsPath, 'utf-8');
const rawSettings: unknown = JSON.parse(stripJsonComments(content));
if (
typeof rawSettings !== 'object' ||
rawSettings === null ||
Array.isArray(rawSettings)
) {
console.error(`VS Code settings file is not a valid JSON object: ${vscodeSettingsPath}`);
return {};
}
return rawSettings as Settings;
}
} catch (error: unknown) {
console.error(`Error loading VS Code settings from ${vscodeSettingsPath}:`, getErrorMessage(error));
}
return {};
}
export type { DnsResolutionOrder } from './settingsSchema.js';
export enum SettingScope {
@@ -632,6 +660,9 @@ export function loadSettings(
workspaceDir,
).getWorkspaceSettingsPath();
// Load VS Code settings as an additional source of configuration
const vscodeSettings = loadVsCodeSettings(workspaceDir);
const loadAndMigrate = (
filePath: string,
scope: SettingScope,
@@ -736,6 +767,14 @@ export function loadSettings(
userSettings = resolveEnvVarsInObject(userResult.settings);
workspaceSettings = resolveEnvVarsInObject(workspaceResult.settings);
// Merge VS Code settings into workspace settings (VS Code settings take precedence)
workspaceSettings = customDeepMerge(
getMergeStrategyForPath,
{},
workspaceSettings,
vscodeSettings,
) as Settings;
// Support legacy theme names
if (userSettings.ui?.theme === 'VS') {
userSettings.ui.theme = DefaultLight.name;
@@ -749,11 +788,13 @@ export function loadSettings(
}
// For the initial trust check, we can only use user and system settings.
// We also include VS Code settings as they may contain trust-related settings
const initialTrustCheckSettings = customDeepMerge(
getMergeStrategyForPath,
{},
systemSettings,
userSettings,
vscodeSettings, // Include VS Code settings
);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings).isTrusted ?? true;
@@ -767,9 +808,18 @@ export function loadSettings(
isTrusted,
);
// Add VS Code settings to the temp merged settings for environment loading
// Since loadEnvironment depends on settings, we need to consider VS Code settings as well
const tempMergedSettingsWithVsCode = customDeepMerge(
getMergeStrategyForPath,
{},
tempMergedSettings,
vscodeSettings,
) as Settings;
// loadEnviroment depends on settings so we have to create a temp version of
// the settings to avoid a cycle
loadEnvironment(tempMergedSettings);
loadEnvironment(tempMergedSettingsWithVsCode);
// Create LoadedSettings first

View File

@@ -202,7 +202,6 @@ const SETTINGS_SCHEMA = {
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文 (Chinese)' },
{ value: 'ru', label: 'Русский (Russian)' },
{ value: 'de', label: 'Deutsch (German)' },
],
},
terminalBell: {
@@ -1008,6 +1007,47 @@ const SETTINGS_SCHEMA = {
},
},
},
lsp: {
type: 'object',
label: 'LSP',
category: 'LSP',
requiresRestart: true,
default: {},
description: 'Settings for the native Language Server Protocol integration.',
showInDialog: false,
properties: {
enabled: {
type: 'boolean',
label: 'Enable LSP',
category: 'LSP',
requiresRestart: true,
default: false,
description:
'Enable the native LSP client to connect to language servers discovered in the workspace.',
showInDialog: false,
},
allowed: {
type: 'array',
label: 'Allow LSP Servers',
category: 'LSP',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Optional allowlist of LSP server names. If set, only matching servers will start.',
showInDialog: false,
},
excluded: {
type: 'array',
label: 'Exclude LSP Servers',
category: 'LSP',
requiresRestart: true,
default: undefined as string[] | undefined,
description:
'Optional blocklist of LSP server names that should not start.',
showInDialog: false,
},
},
},
useSmartEdit: {
type: 'boolean',
label: 'Use Smart Edit',

View File

@@ -460,7 +460,6 @@ describe('gemini.tsx main function kitty protocol', () => {
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
acp: undefined,
experimentalAcp: undefined,
experimentalSkills: undefined,
extensions: undefined,
@@ -640,37 +639,4 @@ describe('startInteractiveUI', () => {
await new Promise((resolve) => setTimeout(resolve, 0));
expect(checkForUpdates).toHaveBeenCalledTimes(1);
});
it('should not check for updates when update nag is disabled', async () => {
const { checkForUpdates } = await import('./ui/utils/updateCheck.js');
const mockInitializationResult = {
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
};
const settingsWithUpdateNagDisabled = {
merged: {
general: {
disableUpdateNag: true,
},
ui: {
hideWindowTitle: false,
},
},
} as LoadedSettings;
await startInteractiveUI(
mockConfig,
settingsWithUpdateNagDisabled,
mockStartupWarnings,
mockWorkspaceRoot,
mockInitializationResult,
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(checkForUpdates).not.toHaveBeenCalled();
});
});

View File

@@ -183,18 +183,16 @@ export async function startInteractiveUI(
},
);
if (!settings.merged.general?.disableUpdateNag) {
checkForUpdates()
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
})
.catch((err) => {
// Silently ignore update check errors.
if (config.getDebugMode()) {
console.error('Update check failed:', err);
}
});
}
checkForUpdates()
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
})
.catch((err) => {
// Silently ignore update check errors.
if (config.getDebugMode()) {
console.error('Update check failed:', err);
}
});
registerCleanup(() => instance.unmount());
}
@@ -250,6 +248,8 @@ export async function main() {
[],
new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()),
argv,
undefined,
{ startLsp: false },
);
if (

File diff suppressed because it is too large Load Diff

View File

@@ -89,9 +89,6 @@ export default {
'No tools available': 'No tools available',
'View or change the approval mode for tool usage':
'View or change the approval mode for tool usage',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}',
'Approval mode set to "{{mode}}"': 'Approval mode set to "{{mode}}"',
'View or change the language setting': 'View or change the language setting',
'change the theme': 'change the theme',
'Select Theme': 'Select Theme',
@@ -1040,6 +1037,7 @@ export default {
'Applying percussive maintenance...',
'Searching for the correct USB orientation...',
'Ensuring the magic smoke stays inside the wires...',
'Rewriting in Rust for no particular reason...',
'Trying to exit Vim...',
'Spinning up the hamster wheel...',
"That's not a bug, it's an undocumented feature...",

View File

@@ -89,10 +89,6 @@ export default {
'No tools available': 'Нет доступных инструментов',
'View or change the approval mode for tool usage':
'Просмотр или изменение режима подтверждения для использования инструментов',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'Недопустимый режим подтверждения "{{arg}}". Допустимые режимы: {{modes}}',
'Approval mode set to "{{mode}}"':
'Режим подтверждения установлен на "{{mode}}"',
'View or change the language setting':
'Просмотр или изменение настроек языка',
'change the theme': 'Изменение темы',
@@ -1060,6 +1056,7 @@ export default {
'Провожу настройку методом тыка...',
'Ищем, какой стороной вставлять флешку...',
'Следим, чтобы волшебный дым не вышел из проводов...',
'Переписываем всё на Rust без особой причины...',
'Пытаемся выйти из Vim...',
'Раскручиваем колесо для хомяка...',
'Это не баг, а фича...',

View File

@@ -88,9 +88,6 @@ export default {
'No tools available': '没有可用工具',
'View or change the approval mode for tool usage':
'查看或更改工具使用的审批模式',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}':
'无效的审批模式 "{{arg}}"。有效模式:{{modes}}',
'Approval mode set to "{{mode}}"': '审批模式已设置为 "{{mode}}"',
'View or change the language setting': '查看或更改语言设置',
'change the theme': '更改主题',
'Select Theme': '选择主题',

View File

@@ -630,67 +630,6 @@ describe('BaseJsonOutputAdapter', () => {
expect(state.blocks).toHaveLength(0);
});
it('should preserve whitespace in thinking content', () => {
const state = adapter.exposeCreateMessageState();
adapter.startAssistantMessage();
adapter.exposeAppendThinking(
state,
'',
'The user just said "Hello"',
null,
);
expect(state.blocks).toHaveLength(1);
expect(state.blocks[0]).toMatchObject({
type: 'thinking',
thinking: 'The user just said "Hello"',
});
// Verify spaces are preserved
const block = state.blocks[0] as { thinking: string };
expect(block.thinking).toContain('user just');
expect(block.thinking).not.toContain('userjust');
});
it('should preserve whitespace when appending multiple thinking fragments', () => {
const state = adapter.exposeCreateMessageState();
adapter.startAssistantMessage();
// Simulate streaming thinking content in fragments
adapter.exposeAppendThinking(state, '', 'The user just', null);
adapter.exposeAppendThinking(state, '', ' said "Hello"', null);
adapter.exposeAppendThinking(
state,
'',
'. This is a simple greeting',
null,
);
expect(state.blocks).toHaveLength(1);
const block = state.blocks[0] as { thinking: string };
// Verify the complete text with all spaces preserved
expect(block.thinking).toBe(
'The user just said "Hello". This is a simple greeting',
);
// Verify specific space preservation
expect(block.thinking).toContain('user just ');
expect(block.thinking).toContain(' said');
expect(block.thinking).toContain('". This');
expect(block.thinking).not.toContain('userjust');
expect(block.thinking).not.toContain('justsaid');
});
it('should preserve leading and trailing whitespace in description', () => {
const state = adapter.exposeCreateMessageState();
adapter.startAssistantMessage();
adapter.exposeAppendThinking(state, '', ' content with spaces ', null);
expect(state.blocks).toHaveLength(1);
const block = state.blocks[0] as { thinking: string };
expect(block.thinking).toBe(' content with spaces ');
});
});
describe('appendToolUse', () => {

View File

@@ -816,18 +816,9 @@ export abstract class BaseJsonOutputAdapter {
parentToolUseId?: string | null,
): void {
const actualParentToolUseId = parentToolUseId ?? null;
// Build fragment without trimming to preserve whitespace in streaming content
// Only filter out null/undefined/empty values
const parts: string[] = [];
if (subject && subject.length > 0) {
parts.push(subject);
}
if (description && description.length > 0) {
parts.push(description);
}
const fragment = parts.join(': ');
const fragment = [subject?.trim(), description?.trim()]
.filter((value) => value && value.length > 0)
.join(': ');
if (!fragment) {
return;
}

View File

@@ -323,68 +323,6 @@ describe('StreamJsonOutputAdapter', () => {
});
});
it('should preserve whitespace in thinking content (issue #1356)', () => {
adapter.processEvent({
type: GeminiEventType.Thought,
value: {
subject: '',
description: 'The user just said "Hello"',
},
});
const message = adapter.finalizeAssistantMessage();
expect(message.message.content).toHaveLength(1);
const block = message.message.content[0] as {
type: string;
thinking: string;
};
expect(block.type).toBe('thinking');
expect(block.thinking).toBe('The user just said "Hello"');
// Verify spaces are preserved
expect(block.thinking).toContain('user just');
expect(block.thinking).not.toContain('userjust');
});
it('should preserve whitespace when streaming multiple thinking fragments (issue #1356)', () => {
// Simulate streaming thinking content in multiple events
adapter.processEvent({
type: GeminiEventType.Thought,
value: {
subject: '',
description: 'The user just',
},
});
adapter.processEvent({
type: GeminiEventType.Thought,
value: {
subject: '',
description: ' said "Hello"',
},
});
adapter.processEvent({
type: GeminiEventType.Thought,
value: {
subject: '',
description: '. This is a simple greeting',
},
});
const message = adapter.finalizeAssistantMessage();
expect(message.message.content).toHaveLength(1);
const block = message.message.content[0] as {
type: string;
thinking: string;
};
expect(block.thinking).toBe(
'The user just said "Hello". This is a simple greeting',
);
// Verify specific spaces are preserved
expect(block.thinking).toContain('user just ');
expect(block.thinking).toContain(' said');
expect(block.thinking).not.toContain('userjust');
expect(block.thinking).not.toContain('justsaid');
});
it('should append tool use from ToolCallRequest events', () => {
adapter.processEvent({
type: GeminiEventType.ToolCallRequest,

View File

@@ -298,9 +298,7 @@ describe('runNonInteractive', () => {
mockConfig,
expect.objectContaining({ name: 'testTool' }),
expect.any(AbortSignal),
expect.objectContaining({
outputUpdateHandler: expect.any(Function),
}),
undefined,
);
// Verify first call has isContinuation: false
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
@@ -773,52 +771,6 @@ describe('runNonInteractive', () => {
);
});
it('should handle API errors in text mode and exit with error code', async () => {
(mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.TEXT);
setupMetricsMock();
// Simulate an API error event (like 401 unauthorized)
const apiErrorEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.Error,
value: {
error: {
message: '401 Incorrect API key provided',
status: 401,
},
},
};
mockGeminiClient.sendMessageStream.mockReturnValue(
createStreamFromEvents([apiErrorEvent]),
);
let thrownError: Error | null = null;
try {
await runNonInteractive(
mockConfig,
mockSettings,
'Test input',
'prompt-id-api-error',
);
// Should not reach here
expect.fail('Expected error to be thrown');
} catch (error) {
thrownError = error as Error;
}
// Should throw with the API error message
expect(thrownError).toBeTruthy();
expect(thrownError?.message).toContain('401');
expect(thrownError?.message).toContain('Incorrect API key provided');
// Verify error was written to stderr
expect(processStderrSpy).toHaveBeenCalled();
const stderrCalls = processStderrSpy.mock.calls;
const errorOutput = stderrCalls.map((call) => call[0]).join('');
expect(errorOutput).toContain('401');
expect(errorOutput).toContain('Incorrect API key provided');
});
it('should handle FatalInputError with custom exit code in JSON format', async () => {
(mockConfig.getOutputFormat as Mock).mockReturnValue(OutputFormat.JSON);
setupMetricsMock();
@@ -1825,84 +1777,4 @@ describe('runNonInteractive', () => {
{ isContinuation: false },
);
});
it('should print tool output to console in text mode (non-Task tools)', async () => {
// Test that tool output is printed to stdout in text mode
const toolCallEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
value: {
callId: 'tool-1',
name: 'run_in_terminal',
args: { command: 'npm outdated' },
isClientInitiated: false,
prompt_id: 'prompt-id-tool-output',
},
};
// Mock tool execution with outputUpdateHandler being called
mockCoreExecuteToolCall.mockImplementation(
async (_config, _request, _signal, options) => {
// Simulate tool calling outputUpdateHandler with output chunks
if (options?.outputUpdateHandler) {
options.outputUpdateHandler('tool-1', 'Package outdated\n');
options.outputUpdateHandler('tool-1', 'npm@1.0.0 -> npm@2.0.0\n');
}
return {
responseParts: [
{
functionResponse: {
id: 'tool-1',
name: 'run_in_terminal',
response: {
output: 'Package outdated\nnpm@1.0.0 -> npm@2.0.0',
},
},
},
],
};
},
);
const firstCallEvents: ServerGeminiStreamEvent[] = [
toolCallEvent,
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 5 } },
},
];
const secondCallEvents: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Dependencies checked' },
{
type: GeminiEventType.Finished,
value: { reason: undefined, usageMetadata: { totalTokenCount: 3 } },
},
];
mockGeminiClient.sendMessageStream
.mockReturnValueOnce(createStreamFromEvents(firstCallEvents))
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive(
mockConfig,
mockSettings,
'Check dependencies',
'prompt-id-tool-output',
);
// Verify that executeToolCall was called with outputUpdateHandler
expect(mockCoreExecuteToolCall).toHaveBeenCalledWith(
mockConfig,
expect.objectContaining({ name: 'run_in_terminal' }),
expect.any(AbortSignal),
expect.objectContaining({
outputUpdateHandler: expect.any(Function),
}),
);
// Verify tool output was written to stdout
expect(processStdoutSpy).toHaveBeenCalledWith('Package outdated\n');
expect(processStdoutSpy).toHaveBeenCalledWith('npm@1.0.0 -> npm@2.0.0\n');
expect(processStdoutSpy).toHaveBeenCalledWith('Dependencies checked');
});
});

View File

@@ -4,11 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {
Config,
ToolCallRequestInfo,
ToolResultDisplay,
} from '@qwen-code/qwen-code-core';
import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core';
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
@@ -312,8 +308,6 @@ export async function runNonInteractive(
config.getContentGeneratorConfig()?.authType,
);
process.stderr.write(`${errorText}\n`);
// Throw error to exit with non-zero code
throw new Error(errorText);
}
}
}
@@ -339,7 +333,7 @@ export async function runNonInteractive(
? options.controlService.permission.getToolCallUpdateCallback()
: undefined;
// Create output handler for Task tool (for subagent execution)
// Only pass outputUpdateHandler for Task tool
const isTaskTool = finalRequestInfo.name === 'task';
const taskToolProgress = isTaskTool
? createTaskToolProgressHandler(
@@ -349,41 +343,20 @@ export async function runNonInteractive(
)
: undefined;
const taskToolProgressHandler = taskToolProgress?.handler;
// Create output handler for non-Task tools in text mode (for console output)
const nonTaskOutputHandler =
!isTaskTool && !adapter
? (callId: string, outputChunk: ToolResultDisplay) => {
// Print tool output to console in text mode
if (typeof outputChunk === 'string') {
process.stdout.write(outputChunk);
} else if (
outputChunk &&
typeof outputChunk === 'object' &&
'ansiOutput' in outputChunk
) {
// Handle ANSI output - just print as string for now
process.stdout.write(String(outputChunk.ansiOutput));
}
}
: undefined;
// Combine output handlers
const outputUpdateHandler =
taskToolProgressHandler || nonTaskOutputHandler;
const toolResponse = await executeToolCall(
config,
finalRequestInfo,
abortController.signal,
outputUpdateHandler || toolCallUpdateCallback
isTaskTool && taskToolProgressHandler
? {
...(outputUpdateHandler && { outputUpdateHandler }),
...(toolCallUpdateCallback && {
onToolCallsUpdate: toolCallUpdateCallback,
}),
outputUpdateHandler: taskToolProgressHandler,
onToolCallsUpdate: toolCallUpdateCallback,
}
: undefined,
: toolCallUpdateCallback
? {
onToolCallsUpdate: toolCallUpdateCallback,
}
: undefined,
);
// Note: In JSON mode, subagent messages are automatically added to the main

View File

@@ -0,0 +1,361 @@
import * as cp from 'node:child_process';
import * as net from 'node:net';
interface PendingRequest {
resolve: (value: unknown) => void;
reject: (reason?: unknown) => void;
timer: NodeJS.Timeout;
}
class JsonRpcConnection {
private buffer = '';
private nextId = 1;
private disposed = false;
private pendingRequests = new Map<string | number, PendingRequest>();
private notificationHandlers: Array<(notification: JsonRpcMessage) => void> =
[];
private requestHandlers: Array<
(request: JsonRpcMessage) => Promise<unknown>
> = [];
constructor(
private readonly writer: (data: string) => void,
private readonly disposer?: () => void,
) {}
listen(readable: NodeJS.ReadableStream): void {
readable.on('data', (chunk: Buffer) => this.handleData(chunk));
readable.on('error', (error) =>
this.disposePending(
error instanceof Error ? error : new Error(String(error)),
),
);
}
send(message: JsonRpcMessage): void {
this.writeMessage(message);
}
onNotification(handler: (notification: JsonRpcMessage) => void): void {
this.notificationHandlers.push(handler);
}
onRequest(handler: (request: JsonRpcMessage) => Promise<unknown>): void {
this.requestHandlers.push(handler);
}
async initialize(params: unknown): Promise<unknown> {
return this.sendRequest('initialize', params);
}
async shutdown(): Promise<void> {
try {
await this.sendRequest('shutdown', {});
} catch (_error) {
// Ignore shutdown errors the server may already be gone.
} finally {
this.end();
}
}
request(method: string, params: unknown): Promise<unknown> {
return this.sendRequest(method, params);
}
end(): void {
if (this.disposed) {
return;
}
this.disposed = true;
this.disposePending();
this.disposer?.();
}
private sendRequest(method: string, params: unknown): Promise<unknown> {
if (this.disposed) {
return Promise.resolve(undefined);
}
const id = this.nextId++;
const payload: JsonRpcMessage = {
jsonrpc: '2.0',
id,
method,
params,
};
const requestPromise = new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`LSP request timeout: ${method}`));
}, 15000);
this.pendingRequests.set(id, { resolve, reject, timer });
});
this.writeMessage(payload);
return requestPromise;
}
private async handleServerRequest(message: JsonRpcMessage): Promise<void> {
const handler = this.requestHandlers[this.requestHandlers.length - 1];
if (!handler) {
this.writeMessage({
jsonrpc: '2.0',
id: message.id,
error: {
code: -32601,
message: `Method not supported: ${message.method}`,
},
});
return;
}
try {
const result = await handler(message);
this.writeMessage({
jsonrpc: '2.0',
id: message.id,
result: result ?? null,
});
} catch (error) {
this.writeMessage({
jsonrpc: '2.0',
id: message.id,
error: {
code: -32603,
message: (error as Error).message ?? 'Internal error',
},
});
}
}
private handleData(chunk: Buffer): void {
if (this.disposed) {
return;
}
this.buffer += chunk.toString('utf8');
while (true) {
const headerEnd = this.buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) {
break;
}
const header = this.buffer.slice(0, headerEnd);
const lengthMatch = /Content-Length:\s*(\d+)/i.exec(header);
if (!lengthMatch) {
this.buffer = this.buffer.slice(headerEnd + 4);
continue;
}
const contentLength = Number(lengthMatch[1]);
const messageStart = headerEnd + 4;
const messageEnd = messageStart + contentLength;
if (this.buffer.length < messageEnd) {
break;
}
const body = this.buffer.slice(messageStart, messageEnd);
this.buffer = this.buffer.slice(messageEnd);
try {
const message = JSON.parse(body);
this.routeMessage(message);
} catch {
// ignore malformed messages
}
}
}
private routeMessage(message: JsonRpcMessage): void {
if (typeof message?.id !== 'undefined' && !message.method) {
const pending = this.pendingRequests.get(message.id);
if (!pending) {
return;
}
clearTimeout(pending.timer);
this.pendingRequests.delete(message.id);
if (message.error) {
pending.reject(
new Error(message.error.message || 'LSP request failed'),
);
} else {
pending.resolve(message.result);
}
return;
}
if (message?.method && typeof message.id !== 'undefined') {
void this.handleServerRequest(message);
return;
}
if (message?.method) {
for (const handler of this.notificationHandlers) {
try {
handler(message);
} catch {
// ignore handler errors
}
}
}
}
private writeMessage(message: JsonRpcMessage): void {
if (this.disposed) {
return;
}
const json = JSON.stringify(message);
const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n`;
this.writer(header + json);
}
private disposePending(error?: Error): void {
for (const [, pending] of Array.from(this.pendingRequests)) {
clearTimeout(pending.timer);
pending.reject(error ?? new Error('LSP connection closed'));
}
this.pendingRequests.clear();
}
}
interface LspConnection {
connection: JsonRpcConnection;
process?: cp.ChildProcess;
socket?: net.Socket;
}
interface JsonRpcMessage {
jsonrpc: string;
id?: number | string;
method?: string;
params?: unknown;
result?: unknown;
error?: {
code: number;
message: string;
data?: unknown;
};
}
export class LspConnectionFactory {
/**
* 创建基于 stdio 的 LSP 连接
*/
static async createStdioConnection(
command: string,
args: string[],
options?: cp.SpawnOptions,
): Promise<LspConnection> {
return new Promise((resolve, reject) => {
const spawnOptions: cp.SpawnOptions = {
stdio: 'pipe',
...options,
};
const processInstance = cp.spawn(command, args, spawnOptions);
const timeoutId = setTimeout(() => {
reject(new Error('LSP server spawn timeout'));
if (!processInstance.killed) {
processInstance.kill();
}
}, 10000);
processInstance.once('error', (error) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to spawn LSP server: ${error.message}`));
});
processInstance.once('spawn', () => {
clearTimeout(timeoutId);
if (!processInstance.stdout || !processInstance.stdin) {
reject(new Error('LSP server stdio not available'));
return;
}
const connection = new JsonRpcConnection(
(payload) => processInstance.stdin?.write(payload),
() => processInstance.stdin?.end(),
);
connection.listen(processInstance.stdout);
processInstance.once('exit', () => connection.end());
processInstance.once('close', () => connection.end());
resolve({
connection,
process: processInstance,
});
});
});
}
/**
* 创建基于 TCP 的 LSP 连接
*/
static async createTcpConnection(
host: string,
port: number,
): Promise<LspConnection> {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host, port });
const timeoutId = setTimeout(() => {
reject(new Error('LSP server connection timeout'));
socket.destroy();
}, 10000);
const onError = (error: Error) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to connect to LSP server: ${error.message}`));
};
socket.once('error', onError);
socket.on('connect', () => {
clearTimeout(timeoutId);
socket.off('error', onError);
const connection = new JsonRpcConnection(
(payload) => socket.write(payload),
() => socket.destroy(),
);
connection.listen(socket);
socket.once('close', () => connection.end());
socket.once('error', () => connection.end());
resolve({
connection,
socket,
});
});
});
}
/**
* 关闭 LSP 连接
*/
static async closeConnection(lspConnection: LspConnection): Promise<void> {
if (lspConnection.connection) {
try {
await lspConnection.connection.shutdown();
} catch (e) {
console.warn('LSP shutdown failed:', e);
} finally {
lspConnection.connection.end();
}
}
if (lspConnection.process && !lspConnection.process.killed) {
lspConnection.process.kill();
}
if (lspConnection.socket && !lspConnection.socket.destroyed) {
lspConnection.socket.destroy();
}
}
}

View File

@@ -0,0 +1,121 @@
import { NativeLspService } from './NativeLspService.js';
import { EventEmitter } from 'events';
// 模拟依赖项
class MockConfig {
rootPath = '/test/workspace';
isTrustedFolder(): boolean {
return true;
}
get(_key: string) {
return undefined;
}
getProjectRoot(): string {
return this.rootPath;
}
}
class MockWorkspaceContext {
rootPath = '/test/workspace';
async fileExists(_path: string): Promise<boolean> {
return _path.endsWith('.json') || _path.includes('package.json');
}
async readFile(_path: string): Promise<string> {
if (_path.includes('.lsp.json')) {
return JSON.stringify({
typescript: {
command: 'typescript-language-server',
args: ['--stdio'],
transport: 'stdio',
},
});
}
return '{}';
}
resolvePath(_path: string): string {
return this.rootPath + '/' + _path;
}
isPathWithinWorkspace(_path: string): boolean {
return true;
}
getDirectories(): string[] {
return [this.rootPath];
}
}
class MockFileDiscoveryService {
async discoverFiles(_root: string, _options: unknown): Promise<string[]> {
// 模拟发现一些文件
return [
'/test/workspace/src/index.ts',
'/test/workspace/src/utils.ts',
'/test/workspace/server.py',
'/test/workspace/main.go',
];
}
shouldIgnoreFile(): boolean {
return false;
}
}
class MockIdeContextStore {
// 模拟 IDE 上下文存储
}
describe('NativeLspService', () => {
let lspService: NativeLspService;
let mockConfig: MockConfig;
let mockWorkspace: MockWorkspaceContext;
let mockFileDiscovery: MockFileDiscoveryService;
let mockIdeStore: MockIdeContextStore;
let eventEmitter: EventEmitter;
beforeEach(() => {
mockConfig = new MockConfig();
mockWorkspace = new MockWorkspaceContext();
mockFileDiscovery = new MockFileDiscoveryService();
mockIdeStore = new MockIdeContextStore();
eventEmitter = new EventEmitter();
lspService = new NativeLspService(
mockConfig as unknown as CoreConfig,
mockWorkspace as unknown as WorkspaceContext,
eventEmitter,
mockFileDiscovery as unknown as FileDiscoveryService,
mockIdeStore as unknown as IdeContextStore,
);
});
test('should initialize correctly', () => {
expect(lspService).toBeDefined();
});
test('should detect languages from workspace files', async () => {
// 这个测试需要修改,因为我们无法直接访问私有方法
await lspService.discoverAndPrepare();
const status = lspService.getStatus();
// 检查服务是否已准备就绪
expect(status).toBeDefined();
});
test('should merge built-in presets with user configs', async () => {
await lspService.discoverAndPrepare();
const status = lspService.getStatus();
// 检查服务是否已准备就绪
expect(status).toBeDefined();
});
});
// 注意:实际的单元测试需要适当的测试框架配置
// 这里只是一个结构示例

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,6 @@ describe('ShellProcessor', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getShouldUseNodePtyShell: vi.fn().mockReturnValue(false),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
getAllowedTools: vi.fn().mockReturnValue([]),
};
context = createMockCommandContext({
@@ -197,35 +196,6 @@ describe('ShellProcessor', () => {
);
});
it('should NOT throw ConfirmationRequiredError when a command matches allowedTools', async () => {
const processor = new ShellProcessor('test-command');
const prompt: PromptPipelineContent = createPromptPipelineContent(
'Do something dangerous: !{rm -rf /}',
);
mockCheckCommandPermissions.mockReturnValue({
allAllowed: false,
disallowedCommands: ['rm -rf /'],
});
(mockConfig.getAllowedTools as Mock).mockReturnValue([
'ShellTool(rm -rf /)',
]);
mockShellExecute.mockReturnValue({
result: Promise.resolve({ ...SUCCESS_RESULT, output: 'deleted' }),
});
const result = await processor.process(prompt, context);
expect(mockShellExecute).toHaveBeenCalledWith(
'rm -rf /',
expect.any(String),
expect.any(Function),
expect.any(Object),
false,
expect.any(Object),
);
expect(result).toEqual([{ text: 'Do something dangerous: deleted' }]);
});
it('should NOT throw ConfirmationRequiredError if a command is not allowed but approval mode is YOLO', async () => {
const processor = new ShellProcessor('test-command');
const prompt: PromptPipelineContent = createPromptPipelineContent(

View File

@@ -7,13 +7,11 @@
import {
ApprovalMode,
checkCommandPermissions,
doesToolInvocationMatch,
escapeShellArg,
getShellConfiguration,
ShellExecutionService,
flatMapTextParts,
} from '@qwen-code/qwen-code-core';
import type { AnyToolInvocation } from '@qwen-code/qwen-code-core';
import type { CommandContext } from '../../ui/commands/types.js';
import type { IPromptProcessor, PromptPipelineContent } from './types.js';
@@ -126,15 +124,6 @@ export class ShellProcessor implements IPromptProcessor {
// Security check on the final, escaped command string.
const { allAllowed, disallowedCommands, blockReason, isHardDenial } =
checkCommandPermissions(command, config, sessionShellAllowlist);
const allowedTools = config.getAllowedTools() || [];
const invocation = {
params: { command },
} as AnyToolInvocation;
const isAllowedBySettings = doesToolInvocationMatch(
'run_shell_command',
invocation,
allowedTools,
);
if (!allAllowed) {
if (isHardDenial) {
@@ -143,17 +132,10 @@ export class ShellProcessor implements IPromptProcessor {
);
}
// If the command is allowed by settings, skip confirmation.
if (isAllowedBySettings) {
continue;
}
// If not a hard denial, respect YOLO mode and auto-approve.
if (config.getApprovalMode() === ApprovalMode.YOLO) {
continue;
if (config.getApprovalMode() !== ApprovalMode.YOLO) {
disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
}
disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
}
}

View File

@@ -925,12 +925,7 @@ export const AppContainer = (props: AppContainerProps) => {
const handleIdePromptComplete = useCallback(
(result: IdeIntegrationNudgeResult) => {
if (result.userSelection === 'yes') {
// Check whether the extension has been pre-installed
if (result.isExtensionPreInstalled) {
handleSlashCommand('/ide enable');
} else {
handleSlashCommand('/ide install');
}
handleSlashCommand('/ide install');
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
} else if (result.userSelection === 'dismiss') {
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);

View File

@@ -38,7 +38,6 @@ export function IdeIntegrationNudge({
);
const { displayName: ideName } = ide;
const isInSandbox = !!process.env['SANDBOX'];
// Assume extension is already installed if the env variables are set.
const isExtensionPreInstalled =
!!process.env['QWEN_CODE_IDE_SERVER_PORT'] &&
@@ -71,15 +70,13 @@ export function IdeIntegrationNudge({
},
];
const installText = isInSandbox
? `Note: In sandbox environments, IDE integration requires manual setup on the host system. If you select Yes, you'll receive instructions on how to set this up.`
: isExtensionPreInstalled
? `If you select Yes, the CLI will connect to your ${
ideName ?? 'editor'
} and have access to your open files and display diffs directly.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
const installText = isExtensionPreInstalled
? `If you select Yes, the CLI will have access to your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`
: `If you select Yes, we'll install an extension that allows the CLI to access your open files and display diffs directly in ${
ideName ?? 'your editor'
}.`;
return (
<Box

View File

@@ -4,28 +4,31 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect } from 'vitest';
import { approvalModeCommand } from './approvalModeCommand.js';
import {
type CommandContext,
CommandKind,
type OpenDialogActionReturn,
type MessageActionReturn,
} from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { LoadedSettings } from '../../config/settings.js';
describe('approvalModeCommand', () => {
let mockContext: CommandContext;
let mockSetApprovalMode: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockSetApprovalMode = vi.fn();
mockContext = createMockCommandContext({
services: {
config: {
getApprovalMode: () => 'default',
setApprovalMode: mockSetApprovalMode,
setApprovalMode: () => {},
},
settings: {
merged: {},
setValue: () => {},
forScope: () => ({}),
} as unknown as LoadedSettings,
},
});
});
@@ -38,7 +41,7 @@ describe('approvalModeCommand', () => {
expect(approvalModeCommand.kind).toBe(CommandKind.BUILT_IN);
});
it('should open approval mode dialog when invoked without arguments', async () => {
it('should open approval mode dialog when invoked', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'',
@@ -48,123 +51,16 @@ describe('approvalModeCommand', () => {
expect(result.dialog).toBe('approval-mode');
});
it('should open approval mode dialog when invoked with whitespace only', async () => {
it('should open approval mode dialog with arguments (ignored)', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
' ',
'some arguments',
)) as OpenDialogActionReturn;
expect(result.type).toBe('dialog');
expect(result.dialog).toBe('approval-mode');
});
describe('direct mode setting (session-only)', () => {
it('should set approval mode to "plan" when argument is "plan"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'plan',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('plan');
expect(mockSetApprovalMode).toHaveBeenCalledWith('plan');
});
it('should set approval mode to "yolo" when argument is "yolo"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'yolo',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('yolo');
expect(mockSetApprovalMode).toHaveBeenCalledWith('yolo');
});
it('should set approval mode to "auto-edit" when argument is "auto-edit"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'auto-edit',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('auto-edit');
expect(mockSetApprovalMode).toHaveBeenCalledWith('auto-edit');
});
it('should set approval mode to "default" when argument is "default"', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'default',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('default');
expect(mockSetApprovalMode).toHaveBeenCalledWith('default');
});
it('should be case-insensitive for mode argument', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'YOLO',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(mockSetApprovalMode).toHaveBeenCalledWith('yolo');
});
it('should handle argument with leading/trailing whitespace', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
' plan ',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(mockSetApprovalMode).toHaveBeenCalledWith('plan');
});
});
describe('invalid mode argument', () => {
it('should return error for invalid mode', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'invalid-mode',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('error');
expect(result.content).toContain('invalid-mode');
expect(result.content).toContain('plan');
expect(result.content).toContain('yolo');
expect(mockSetApprovalMode).not.toHaveBeenCalled();
});
});
describe('untrusted folder handling', () => {
it('should return error when setApprovalMode throws (e.g., untrusted folder)', async () => {
const errorMessage =
'Cannot enable privileged approval modes in an untrusted folder.';
mockSetApprovalMode.mockImplementation(() => {
throw new Error(errorMessage);
});
const result = (await approvalModeCommand.action?.(
mockContext,
'yolo',
)) as MessageActionReturn;
expect(result.type).toBe('message');
expect(result.messageType).toBe('error');
expect(result.content).toBe(errorMessage);
});
});
it('should not have subcommands', () => {
expect(approvalModeCommand.subCommands).toBeUndefined();
});

View File

@@ -8,25 +8,9 @@ import type {
SlashCommand,
CommandContext,
OpenDialogActionReturn,
MessageActionReturn,
} from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
import type { ApprovalMode } from '@qwen-code/qwen-code-core';
import { APPROVAL_MODES } from '@qwen-code/qwen-code-core';
/**
* Parses the argument string and returns the corresponding ApprovalMode if valid.
* Returns undefined if the argument is empty or not a valid mode.
*/
function parseApprovalModeArg(arg: string): ApprovalMode | undefined {
const trimmed = arg.trim().toLowerCase();
if (!trimmed) {
return undefined;
}
// Match against valid approval modes (case-insensitive)
return APPROVAL_MODES.find((mode) => mode.toLowerCase() === trimmed);
}
export const approvalModeCommand: SlashCommand = {
name: 'approval-mode',
@@ -35,49 +19,10 @@ export const approvalModeCommand: SlashCommand = {
},
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
args: string,
): Promise<OpenDialogActionReturn | MessageActionReturn> => {
const mode = parseApprovalModeArg(args);
// If no argument provided, open the dialog
if (!args.trim()) {
return {
type: 'dialog',
dialog: 'approval-mode',
};
}
// If invalid argument, return error message with valid options
if (!mode) {
return {
type: 'message',
messageType: 'error',
content: t('Invalid approval mode "{{arg}}". Valid modes: {{modes}}', {
arg: args.trim(),
modes: APPROVAL_MODES.join(', '),
}),
};
}
// Set the mode for current session only (not persisted)
const { config } = context.services;
if (config) {
try {
config.setApprovalMode(mode);
} catch (e) {
return {
type: 'message',
messageType: 'error',
content: (e as Error).message,
};
}
}
return {
type: 'message',
messageType: 'info',
content: t('Approval mode set to "{{mode}}"', { mode }),
};
},
_context: CommandContext,
_args: string,
): Promise<OpenDialogActionReturn> => ({
type: 'dialog',
dialog: 'approval-mode',
}),
};

View File

@@ -191,23 +191,11 @@ export const ideCommand = async (): Promise<SlashCommand> => {
kind: CommandKind.BUILT_IN,
action: async (context) => {
const installer = getIdeInstaller(currentIDE);
const isSandBox = !!process.env['SANDBOX'];
if (isSandBox) {
context.ui.addItem(
{
type: 'info',
text: `IDE integration needs to be installed on the host. If you have already installed it, you can directly connect the ide`,
},
Date.now(),
);
return;
}
if (!installer) {
const ideName = ideClient.getDetectedIdeDisplayName();
context.ui.addItem(
{
type: 'error',
text: `Automatic installation is not supported for ${ideName}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
text: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
},
Date.now(),
);

View File

@@ -87,13 +87,7 @@ export async function showResumeSessionPicker(
let selectedId: string | undefined;
const { unmount, waitUntilExit } = render(
<KeypressProvider
kittyProtocolEnabled={false}
pasteWorkaround={
process.platform === 'win32' ||
parseInt(process.versions.node.split('.')[0], 10) < 20
}
>
<KeypressProvider kittyProtocolEnabled={false}>
<StandalonePickerScreen
sessionService={sessionService}
onSelect={(id) => {

View File

@@ -6,11 +6,7 @@
import { vi, type Mock, type MockInstance } from 'vitest';
import type { Config } from '@qwen-code/qwen-code-core';
import {
OutputFormat,
FatalInputError,
ToolErrorType,
} from '@qwen-code/qwen-code-core';
import { OutputFormat, FatalInputError } from '@qwen-code/qwen-code-core';
import {
getErrorMessage,
handleError,
@@ -69,7 +65,6 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
describe('errors', () => {
let mockConfig: Config;
let processExitSpy: MockInstance;
let processStderrWriteSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
@@ -79,11 +74,6 @@ describe('errors', () => {
// Mock console.error
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Mock process.stderr.write
processStderrWriteSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
// Mock process.exit to throw instead of actually exiting
processExitSpy = vi.spyOn(process, 'exit').mockImplementation((code) => {
throw new Error(`process.exit called with code: ${code}`);
@@ -94,13 +84,11 @@ describe('errors', () => {
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'test' }),
getDebugMode: vi.fn().mockReturnValue(true),
isInteractive: vi.fn().mockReturnValue(false),
} as unknown as Config;
});
afterEach(() => {
consoleErrorSpy.mockRestore();
processStderrWriteSpy.mockRestore();
processExitSpy.mockRestore();
});
@@ -444,87 +432,6 @@ describe('errors', () => {
expect(processExitSpy).not.toHaveBeenCalled();
});
});
describe('permission denied warnings', () => {
it('should show warning when EXECUTION_DENIED in non-interactive text mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining(
'Warning: Tool "test-tool" requires user approval',
),
);
expect(processStderrWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('use the -y flag (YOLO mode)'),
);
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning when EXECUTION_DENIED in interactive mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(true);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning when EXECUTION_DENIED in JSON mode', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.JSON);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.EXECUTION_DENIED,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
it('should not show warning for non-EXECUTION_DENIED errors', () => {
(mockConfig.getDebugMode as Mock).mockReturnValue(false);
(mockConfig.isInteractive as Mock).mockReturnValue(false);
(
mockConfig.getOutputFormat as ReturnType<typeof vi.fn>
).mockReturnValue(OutputFormat.TEXT);
handleToolError(
toolName,
toolError,
mockConfig,
ToolErrorType.FILE_NOT_FOUND,
);
expect(processStderrWriteSpy).not.toHaveBeenCalled();
expect(processExitSpy).not.toHaveBeenCalled();
});
});
});
describe('handleCancellationError', () => {

View File

@@ -11,7 +11,6 @@ import {
parseAndFormatApiError,
FatalTurnLimitedError,
FatalCancellationError,
ToolErrorType,
} from '@qwen-code/qwen-code-core';
export function getErrorMessage(error: unknown): string {
@@ -103,24 +102,10 @@ export function handleToolError(
toolName: string,
toolError: Error,
config: Config,
errorCode?: string | number,
_errorCode?: string | number,
resultDisplay?: string,
): void {
// Check if this is a permission denied error in non-interactive mode
const isExecutionDenied = errorCode === ToolErrorType.EXECUTION_DENIED;
const isNonInteractive = !config.isInteractive();
const isTextMode = config.getOutputFormat() === OutputFormat.TEXT;
// Show warning for permission denied errors in non-interactive text mode
if (isExecutionDenied && isNonInteractive && isTextMode) {
const warningMessage =
`Warning: Tool "${toolName}" requires user approval but cannot execute in non-interactive mode.\n` +
`To enable automatic tool execution, use the -y flag (YOLO mode):\n` +
`Example: qwen -p 'your prompt' -y\n\n`;
process.stderr.write(warningMessage);
}
// Always log detailed error in debug mode
// Always just log to stderr; JSON/streaming formatting happens in the tool_result block elsewhere
if (config.getDebugMode()) {
console.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,

View File

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

View File

@@ -61,6 +61,10 @@ import { ToolRegistry } from '../tools/tool-registry.js';
import { WebFetchTool } from '../tools/web-fetch.js';
import { WebSearchTool } from '../tools/web-search/index.js';
import { WriteFileTool } from '../tools/write-file.js';
import { LspWorkspaceSymbolTool } from '../tools/lsp-workspace-symbol.js';
import { LspGoToDefinitionTool } from '../tools/lsp-go-to-definition.js';
import { LspFindReferencesTool } from '../tools/lsp-find-references.js';
import type { LspClient } from '../lsp/types.js';
// Other modules
import { ideContextStore } from '../ide/ideContext.js';
@@ -281,6 +285,12 @@ export interface ConfigParameters {
toolCallCommand?: string;
mcpServerCommand?: string;
mcpServers?: Record<string, MCPServerConfig>;
lsp?: {
enabled?: boolean;
allowed?: string[];
excluded?: string[];
};
lspClient?: LspClient;
userMemory?: string;
geminiMdFileCount?: number;
approvalMode?: ApprovalMode;
@@ -413,6 +423,10 @@ export class Config {
private readonly toolCallCommand: string | undefined;
private readonly mcpServerCommand: string | undefined;
private mcpServers: Record<string, MCPServerConfig> | undefined;
private readonly lspEnabled: boolean;
private readonly lspAllowed?: string[];
private readonly lspExcluded?: string[];
private lspClient?: LspClient;
private sessionSubagents: SubagentConfig[];
private userMemory: string;
private sdkMode: boolean;
@@ -521,6 +535,10 @@ export class Config {
this.toolCallCommand = params.toolCallCommand;
this.mcpServerCommand = params.mcpServerCommand;
this.mcpServers = params.mcpServers;
this.lspEnabled = params.lsp?.enabled ?? false;
this.lspAllowed = params.lsp?.allowed?.filter(Boolean);
this.lspExcluded = params.lsp?.excluded?.filter(Boolean);
this.lspClient = params.lspClient;
this.sessionSubagents = params.sessionSubagents ?? [];
this.sdkMode = params.sdkMode ?? false;
this.userMemory = params.userMemory ?? '';
@@ -896,6 +914,32 @@ export class Config {
this.mcpServers = { ...this.mcpServers, ...servers };
}
isLspEnabled(): boolean {
return this.lspEnabled;
}
getLspAllowed(): string[] | undefined {
return this.lspAllowed;
}
getLspExcluded(): string[] | undefined {
return this.lspExcluded;
}
getLspClient(): LspClient | undefined {
return this.lspClient;
}
/**
* Allows wiring an LSP client after Config construction but before initialize().
*/
setLspClient(client: LspClient | undefined): void {
if (this.initialized) {
throw new Error('Cannot set LSP client after initialization');
}
this.lspClient = client;
}
getSessionSubagents(): SubagentConfig[] {
return this.sessionSubagents;
}
@@ -1403,6 +1447,11 @@ export class Config {
if (this.getWebSearchConfig()) {
registerCoreTool(WebSearchTool, this);
}
if (this.isLspEnabled() && this.getLspClient()) {
registerCoreTool(LspGoToDefinitionTool, this);
registerCoreTool(LspFindReferencesTool, this);
registerCoreTool(LspWorkspaceSymbolTool, this);
}
await registry.discoverAllTools();
console.debug('ToolRegistry created', registry.getAllToolNames());

View File

@@ -824,6 +824,7 @@ export class CoreToolScheduler {
*/
const shouldAutoDeny =
!this.config.isInteractive() &&
!this.config.getIdeMode() &&
!this.config.getExperimentalZedIntegration() &&
this.config.getInputFormat() !== InputFormat.STREAM_JSON;

View File

@@ -752,8 +752,6 @@ export class OpenAIContentConverter {
usage.prompt_tokens_details?.cached_tokens ??
extendedUsage.cached_tokens ??
0;
const thinkingTokens =
usage.completion_tokens_details?.reasoning_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
@@ -771,7 +769,6 @@ export class OpenAIContentConverter {
candidatesTokenCount: finalCompletionTokens,
totalTokenCount: totalTokens,
cachedContentTokenCount: cachedTokens,
thoughtsTokenCount: thinkingTokens,
};
}

View File

@@ -317,22 +317,15 @@ export class ContentGenerationPipeline {
}
private buildReasoningConfig(): Record<string, unknown> {
// Reasoning configuration for OpenAI-compatible endpoints is highly fragmented.
// For example, across common providers and models:
//
// - deepseek-reasoner — thinking is enabled by default and cannot be disabled
// - glm-4.7 — thinking is enabled by default; can be disabled via `extra_body.thinking.enabled`
// - kimi-k2-thinking — thinking is enabled by default and cannot be disabled
// - gpt-5.x series — thinking is enabled by default; can be disabled via `reasoning.effort`
// - qwen3 series — model-dependent; can be manually disabled via `extra_body.enable_thinking`
//
// Given this inconsistency, we choose not to set any reasoning config here and
// instead rely on each models default behavior.
const reasoning = this.contentGeneratorConfig.reasoning;
// We plan to introduce provider- and model-specific settings to enable more
// fine-grained control over reasoning configuration.
if (reasoning === false) {
return {};
}
return {};
return {
reasoning_effort: reasoning?.effort ?? 'medium',
};
}
/**

View File

@@ -58,6 +58,8 @@ export class DefaultOpenAICompatibleProvider
}
getDefaultGenerationConfig(): GenerateContentConfig {
return {};
return {
topP: 0.95,
};
}
}

View File

@@ -38,7 +38,6 @@ export * from './utils/quotaErrorDetection.js';
export * from './utils/fileUtils.js';
export * from './utils/retry.js';
export * from './utils/shell-utils.js';
export * from './utils/tool-utils.js';
export * from './utils/terminalSerializer.js';
export * from './utils/systemEncoding.js';
export * from './utils/textUtils.js';
@@ -86,6 +85,7 @@ export * from './skills/index.js';
// Export prompt logic
export * from './prompts/mcp-prompts.js';
export * from './lsp/types.js';
// Export specific tool logic
export * from './tools/read-file.js';
@@ -100,6 +100,8 @@ export * from './tools/memoryTool.js';
export * from './tools/shell.js';
export * from './tools/web-search/index.js';
export * from './tools/read-many-files.js';
export * from './tools/lsp-go-to-definition.js';
export * from './tools/lsp-find-references.js';
export * from './tools/mcp-client.js';
export * from './tools/mcp-client-manager.js';
export * from './tools/mcp-tool.js';

View File

@@ -0,0 +1,58 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export interface LspPosition {
line: number;
character: number;
}
export interface LspRange {
start: LspPosition;
end: LspPosition;
}
export interface LspLocation {
uri: string;
range: LspRange;
}
export interface LspLocationWithServer extends LspLocation {
serverName?: string;
}
export interface LspSymbolInformation {
name: string;
kind?: string;
location: LspLocation;
containerName?: string;
serverName?: string;
}
export interface LspReference extends LspLocationWithServer {
readonly serverName?: string;
}
export interface LspDefinition extends LspLocationWithServer {
readonly serverName?: string;
}
export interface LspClient {
workspaceSymbols(
query: string,
limit?: number,
): Promise<LspSymbolInformation[]>;
definitions(
location: LspLocation,
serverName?: string,
limit?: number,
): Promise<LspDefinition[]>;
references(
location: LspLocation,
serverName?: string,
includeDeclaration?: boolean,
limit?: number,
): Promise<LspReference[]>;
}

View File

@@ -589,7 +589,7 @@ describe('ShellExecutionService child_process fallback', () => {
expect(result.error).toBeNull();
expect(result.aborted).toBe(false);
expect(result.output).toBe('file1.txt\na warning');
expect(handle.pid).toBe(12345);
expect(handle.pid).toBe(undefined);
expect(onOutputEventMock).toHaveBeenCalledWith({
type: 'data',
@@ -829,7 +829,7 @@ describe('ShellExecutionService child_process fallback', () => {
[],
expect.objectContaining({
shell: true,
detached: true,
detached: false,
}),
);
});

View File

@@ -7,7 +7,7 @@
import stripAnsi from 'strip-ansi';
import type { PtyImplementation } from '../utils/getPty.js';
import { getPty } from '../utils/getPty.js';
import { spawn as cpSpawn, spawnSync } from 'node:child_process';
import { spawn as cpSpawn } from 'node:child_process';
import { TextDecoder } from 'node:util';
import os from 'node:os';
import type { IPty } from '@lydell/node-pty';
@@ -98,48 +98,6 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
return lines.join('\n').trimEnd();
};
interface ProcessCleanupStrategy {
killPty(pid: number, pty: ActivePty): void;
killChildProcesses(pids: Set<number>): void;
}
const windowsStrategy: ProcessCleanupStrategy = {
killPty: (_pid, pty) => {
pty.ptyProcess.kill();
},
killChildProcesses: (pids) => {
if (pids.size > 0) {
try {
const args = ['/f', '/t'];
for (const pid of pids) {
args.push('/pid', pid.toString());
}
spawnSync('taskkill', args);
} catch {
// ignore
}
}
},
};
const posixStrategy: ProcessCleanupStrategy = {
killPty: (pid, _pty) => {
process.kill(-pid, 'SIGKILL');
},
killChildProcesses: (pids) => {
for (const pid of pids) {
try {
process.kill(-pid, 'SIGKILL');
} catch {
// ignore
}
}
},
};
const getCleanupStrategy = () =>
os.platform() === 'win32' ? windowsStrategy : posixStrategy;
/**
* A centralized service for executing shell commands with robust process
* management, cross-platform compatibility, and streaming output capabilities.
@@ -148,29 +106,6 @@ const getCleanupStrategy = () =>
export class ShellExecutionService {
private static activePtys = new Map<number, ActivePty>();
private static activeChildProcesses = new Set<number>();
static cleanup() {
const strategy = getCleanupStrategy();
// Cleanup PTYs
for (const [pid, pty] of this.activePtys) {
try {
strategy.killPty(pid, pty);
} catch {
// ignore
}
}
// Cleanup child processes
strategy.killChildProcesses(this.activeChildProcesses);
}
static {
process.on('exit', () => {
ShellExecutionService.cleanup();
});
}
/**
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
*
@@ -229,7 +164,7 @@ export class ShellExecutionService {
stdio: ['ignore', 'pipe', 'pipe'],
windowsVerbatimArguments: true,
shell: isWindows ? true : 'bash',
detached: true,
detached: !isWindows,
env: {
...process.env,
QWEN_CODE: '1',
@@ -346,13 +281,9 @@ export class ShellExecutionService {
abortSignal.addEventListener('abort', abortHandler, { once: true });
if (child.pid) {
this.activeChildProcesses.add(child.pid);
}
child.on('exit', (code, signal) => {
if (child.pid) {
this.activeChildProcesses.delete(child.pid);
this.activePtys.delete(child.pid);
}
handleExit(code, signal);
});
@@ -379,7 +310,7 @@ export class ShellExecutionService {
}
});
return { pid: child.pid, result };
return { pid: undefined, result };
} catch (e) {
const error = e as Error;
return {

View File

@@ -0,0 +1,308 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import type { Config } from '../config/config.js';
import type { LspClient, LspLocation, LspReference } from '../lsp/types.js';
export interface LspFindReferencesParams {
/**
* Symbol name to resolve if a file/position is not provided.
*/
symbol?: string;
/**
* File path (absolute or workspace-relative).
* Use together with `line` (1-based) and optional `character` (1-based).
*/
file?: string;
/**
* File URI (e.g., file:///path/to/file).
* Use together with `line` (1-based) and optional `character` (1-based).
*/
uri?: string;
/**
* 1-based line number when targeting a specific file location.
*/
line?: number;
/**
* 1-based character/column number when targeting a specific file location.
*/
character?: number;
/**
* Whether to include the declaration in results (default: false).
*/
includeDeclaration?: boolean;
/**
* Optional server name override.
*/
serverName?: string;
/**
* Optional maximum number of results.
*/
limit?: number;
}
type ResolvedTarget =
| {
location: LspLocation;
description: string;
serverName?: string;
fromSymbol: boolean;
}
| { error: string };
class LspFindReferencesInvocation extends BaseToolInvocation<
LspFindReferencesParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: LspFindReferencesParams,
) {
super(params);
}
getDescription(): string {
if (this.params.symbol) {
return `LSP find-references查引用 for symbol "${this.params.symbol}"`;
}
if (this.params.file && this.params.line !== undefined) {
return `LSP find-references查引用 at ${this.params.file}:${this.params.line}:${this.params.character ?? 1}`;
}
if (this.params.uri && this.params.line !== undefined) {
return `LSP find-references查引用 at ${this.params.uri}:${this.params.line}:${this.params.character ?? 1}`;
}
return 'LSP find-references查引用';
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const client = this.config.getLspClient();
if (!client || !this.config.isLspEnabled()) {
const message =
'LSP find-references is unavailable (LSP disabled or not initialized).';
return { llmContent: message, returnDisplay: message };
}
const target = await this.resolveTarget(client);
if ('error' in target) {
return { llmContent: target.error, returnDisplay: target.error };
}
const limit = this.params.limit ?? 50;
let references: LspReference[] = [];
try {
references = await client.references(
target.location,
target.serverName,
this.params.includeDeclaration ?? false,
limit,
);
} catch (error) {
const message = `LSP find-references failed: ${
(error as Error)?.message || String(error)
}`;
return { llmContent: message, returnDisplay: message };
}
if (!references.length) {
const message = `No references found for ${target.description}.`;
return { llmContent: message, returnDisplay: message };
}
const workspaceRoot = this.config.getProjectRoot();
const lines = references
.slice(0, limit)
.map(
(reference, index) =>
`${index + 1}. ${this.formatLocation(reference, workspaceRoot)}`,
);
const heading = `References for ${target.description}:`;
return {
llmContent: [heading, ...lines].join('\n'),
returnDisplay: lines.join('\n'),
};
}
private async resolveTarget(
client: Pick<LspClient, 'workspaceSymbols'>,
): Promise<ResolvedTarget> {
const workspaceRoot = this.config.getProjectRoot();
const lineProvided = typeof this.params.line === 'number';
const character = this.params.character ?? 1;
if ((this.params.file || this.params.uri) && lineProvided) {
const uri = this.resolveUri(workspaceRoot);
if (!uri) {
return {
error:
'A valid file path or URI is required when specifying a line/character.',
};
}
const position = {
line: Math.max(0, Math.floor((this.params.line ?? 1) - 1)),
character: Math.max(0, Math.floor(character - 1)),
};
const location: LspLocation = {
uri,
range: { start: position, end: position },
};
const description = this.formatLocation(
{ ...location, serverName: this.params.serverName },
workspaceRoot,
);
return {
location,
description,
serverName: this.params.serverName,
fromSymbol: false,
};
}
if (this.params.symbol) {
try {
const symbols = await client.workspaceSymbols(this.params.symbol, 5);
if (!symbols.length) {
return {
error: `No symbols found for query "${this.params.symbol}".`,
};
}
const top = symbols[0];
return {
location: top.location,
description: `symbol "${this.params.symbol}"`,
serverName: this.params.serverName ?? top.serverName,
fromSymbol: true,
};
} catch (error) {
return {
error: `Workspace symbol search failed: ${
(error as Error)?.message || String(error)
}`,
};
}
}
return {
error:
'Provide a symbol name or a file plus line (and optional character) to use find-references.',
};
}
private resolveUri(workspaceRoot: string): string | null {
if (this.params.uri) {
if (
this.params.uri.startsWith('file://') ||
this.params.uri.includes('://')
) {
return this.params.uri;
}
const absoluteUriPath = path.isAbsolute(this.params.uri)
? this.params.uri
: path.resolve(workspaceRoot, this.params.uri);
return pathToFileURL(absoluteUriPath).toString();
}
if (this.params.file) {
const absolutePath = path.isAbsolute(this.params.file)
? this.params.file
: path.resolve(workspaceRoot, this.params.file);
return pathToFileURL(absolutePath).toString();
}
return null;
}
private formatLocation(
location: LspReference | (LspLocation & { serverName?: string }),
workspaceRoot: string,
): string {
const start = location.range.start;
let filePath = location.uri;
if (filePath.startsWith('file://')) {
filePath = fileURLToPath(filePath);
filePath = path.relative(workspaceRoot, filePath) || '.';
}
const serverSuffix =
location.serverName && location.serverName !== ''
? ` [${location.serverName}]`
: '';
return `${filePath}:${(start.line ?? 0) + 1}:${(start.character ?? 0) + 1}${serverSuffix}`;
}
}
export class LspFindReferencesTool extends BaseDeclarativeTool<
LspFindReferencesParams,
ToolResult
> {
static readonly Name = ToolNames.LSP_FIND_REFERENCES;
constructor(private readonly config: Config) {
super(
LspFindReferencesTool.Name,
ToolDisplayNames.LSP_FIND_REFERENCES,
'Use LSP find-references for a symbol or a specific file location查引用优先于 grep 搜索)。',
Kind.Other,
{
type: 'object',
properties: {
symbol: {
type: 'string',
description:
'Symbol name to resolve when a file/position is not provided.',
},
file: {
type: 'string',
description:
'File path (absolute or workspace-relative). Requires `line`.',
},
uri: {
type: 'string',
description:
'File URI (file:///...). Requires `line` when provided.',
},
line: {
type: 'number',
description: '1-based line number for the target location.',
},
character: {
type: 'number',
description:
'1-based character/column number for the target location.',
},
includeDeclaration: {
type: 'boolean',
description:
'Include the declaration itself when looking up references.',
},
serverName: {
type: 'string',
description: 'Optional LSP server name to target.',
},
limit: {
type: 'number',
description: 'Optional maximum number of results to return.',
},
},
},
false,
false,
);
}
protected createInvocation(
params: LspFindReferencesParams,
): ToolInvocation<LspFindReferencesParams, ToolResult> {
return new LspFindReferencesInvocation(this.config, params);
}
}

View File

@@ -0,0 +1,308 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import type { Config } from '../config/config.js';
import type { LspClient, LspDefinition, LspLocation } from '../lsp/types.js';
export interface LspGoToDefinitionParams {
/**
* Symbol name to resolve if a file/position is not provided.
*/
symbol?: string;
/**
* File path (absolute or workspace-relative).
* Use together with `line` (1-based) and optional `character` (1-based).
*/
file?: string;
/**
* File URI (e.g., file:///path/to/file).
* Use together with `line` (1-based) and optional `character` (1-based).
*/
uri?: string;
/**
* 1-based line number when targeting a specific file location.
*/
line?: number;
/**
* 1-based character/column number when targeting a specific file location.
*/
character?: number;
/**
* Optional server name override.
*/
serverName?: string;
/**
* Optional maximum number of results.
*/
limit?: number;
}
type ResolvedTarget =
| {
location: LspLocation;
description: string;
serverName?: string;
fromSymbol: boolean;
}
| { error: string };
class LspGoToDefinitionInvocation extends BaseToolInvocation<
LspGoToDefinitionParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: LspGoToDefinitionParams,
) {
super(params);
}
getDescription(): string {
if (this.params.symbol) {
return `LSP go-to-definition跳转定义 for symbol "${this.params.symbol}"`;
}
if (this.params.file && this.params.line !== undefined) {
return `LSP go-to-definition跳转定义 at ${this.params.file}:${this.params.line}:${this.params.character ?? 1}`;
}
if (this.params.uri && this.params.line !== undefined) {
return `LSP go-to-definition跳转定义 at ${this.params.uri}:${this.params.line}:${this.params.character ?? 1}`;
}
return 'LSP go-to-definition跳转定义';
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const client = this.config.getLspClient();
if (!client || !this.config.isLspEnabled()) {
const message =
'LSP go-to-definition is unavailable (LSP disabled or not initialized).';
return { llmContent: message, returnDisplay: message };
}
const target = await this.resolveTarget(client);
if ('error' in target) {
return { llmContent: target.error, returnDisplay: target.error };
}
const limit = this.params.limit ?? 20;
let definitions: LspDefinition[] = [];
try {
definitions = await client.definitions(
target.location,
target.serverName,
limit,
);
} catch (error) {
const message = `LSP go-to-definition failed: ${
(error as Error)?.message || String(error)
}`;
return { llmContent: message, returnDisplay: message };
}
// Fallback to the resolved symbol location if the server does not return definitions.
if (!definitions.length && target.fromSymbol) {
definitions = [
{
...target.location,
serverName: target.serverName,
},
];
}
if (!definitions.length) {
const message = `No definitions found for ${target.description}.`;
return { llmContent: message, returnDisplay: message };
}
const workspaceRoot = this.config.getProjectRoot();
const lines = definitions
.slice(0, limit)
.map(
(definition, index) =>
`${index + 1}. ${this.formatLocation(definition, workspaceRoot)}`,
);
const heading = `Definitions for ${target.description}:`;
return {
llmContent: [heading, ...lines].join('\n'),
returnDisplay: lines.join('\n'),
};
}
private async resolveTarget(
client: Pick<LspClient, 'workspaceSymbols'>,
): Promise<ResolvedTarget> {
const workspaceRoot = this.config.getProjectRoot();
const lineProvided = typeof this.params.line === 'number';
const character = this.params.character ?? 1;
if ((this.params.file || this.params.uri) && lineProvided) {
const uri = this.resolveUri(workspaceRoot);
if (!uri) {
return {
error:
'A valid file path or URI is required when specifying a line/character.',
};
}
const position = {
line: Math.max(0, Math.floor((this.params.line ?? 1) - 1)),
character: Math.max(0, Math.floor(character - 1)),
};
const location: LspLocation = {
uri,
range: { start: position, end: position },
};
const description = this.formatLocation(
{ ...location, serverName: this.params.serverName },
workspaceRoot,
);
return {
location,
description,
serverName: this.params.serverName,
fromSymbol: false,
};
}
if (this.params.symbol) {
try {
const symbols = await client.workspaceSymbols(this.params.symbol, 5);
if (!symbols.length) {
return {
error: `No symbols found for query "${this.params.symbol}".`,
};
}
const top = symbols[0];
return {
location: top.location,
description: `symbol "${this.params.symbol}"`,
serverName: this.params.serverName ?? top.serverName,
fromSymbol: true,
};
} catch (error) {
return {
error: `Workspace symbol search failed: ${
(error as Error)?.message || String(error)
}`,
};
}
}
return {
error:
'Provide a symbol name or a file plus line (and optional character) to use go-to-definition.',
};
}
private resolveUri(workspaceRoot: string): string | null {
if (this.params.uri) {
if (
this.params.uri.startsWith('file://') ||
this.params.uri.includes('://')
) {
return this.params.uri;
}
const absoluteUriPath = path.isAbsolute(this.params.uri)
? this.params.uri
: path.resolve(workspaceRoot, this.params.uri);
return pathToFileURL(absoluteUriPath).toString();
}
if (this.params.file) {
const absolutePath = path.isAbsolute(this.params.file)
? this.params.file
: path.resolve(workspaceRoot, this.params.file);
return pathToFileURL(absolutePath).toString();
}
return null;
}
private formatLocation(
location: LspDefinition | (LspLocation & { serverName?: string }),
workspaceRoot: string,
): string {
const start = location.range.start;
let filePath = location.uri;
if (filePath.startsWith('file://')) {
filePath = fileURLToPath(filePath);
filePath = path.relative(workspaceRoot, filePath) || '.';
}
const serverSuffix =
location.serverName && location.serverName !== ''
? ` [${location.serverName}]`
: '';
return `${filePath}:${(start.line ?? 0) + 1}:${(start.character ?? 0) + 1}${serverSuffix}`;
}
}
export class LspGoToDefinitionTool extends BaseDeclarativeTool<
LspGoToDefinitionParams,
ToolResult
> {
static readonly Name = ToolNames.LSP_GO_TO_DEFINITION;
constructor(private readonly config: Config) {
super(
LspGoToDefinitionTool.Name,
ToolDisplayNames.LSP_GO_TO_DEFINITION,
'Use LSP go-to-definition for a symbol or a specific file location跳转定义优先于 grep 搜索)。',
Kind.Other,
{
type: 'object',
properties: {
symbol: {
type: 'string',
description:
'Symbol name to resolve when a file/position is not provided.',
},
file: {
type: 'string',
description:
'File path (absolute or workspace-relative). Requires `line`.',
},
uri: {
type: 'string',
description:
'File URI (file:///...). Requires `line` when provided.',
},
line: {
type: 'number',
description: '1-based line number for the target location.',
},
character: {
type: 'number',
description:
'1-based character/column number for the target location.',
},
serverName: {
type: 'string',
description: 'Optional LSP server name to target.',
},
limit: {
type: 'number',
description: 'Optional maximum number of results to return.',
},
},
},
false,
false,
);
}
protected createInvocation(
params: LspGoToDefinitionParams,
): ToolInvocation<LspGoToDefinitionParams, ToolResult> {
return new LspGoToDefinitionInvocation(this.config, params);
}
}

View File

@@ -0,0 +1,180 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ToolInvocation, ToolResult } from './tools.js';
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import { ToolDisplayNames, ToolNames } from './tool-names.js';
import type { Config } from '../config/config.js';
import type { LspSymbolInformation } from '../lsp/types.js';
export interface LspWorkspaceSymbolParams {
/**
* Query string to search symbols (e.g., function or class name).
*/
query: string;
/**
* Maximum number of results to return.
*/
limit?: number;
}
class LspWorkspaceSymbolInvocation extends BaseToolInvocation<
LspWorkspaceSymbolParams,
ToolResult
> {
constructor(
private readonly config: Config,
params: LspWorkspaceSymbolParams,
) {
super(params);
}
getDescription(): string {
return `LSP workspace symbol search按名称找定义/实现/引用) for "${this.params.query}"`;
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const client = this.config.getLspClient();
if (!client || !this.config.isLspEnabled()) {
const message =
'LSP workspace symbol search is unavailable (LSP disabled or not initialized).';
return { llmContent: message, returnDisplay: message };
}
const limit = this.params.limit ?? 20;
let symbols: LspSymbolInformation[] = [];
try {
symbols = await client.workspaceSymbols(this.params.query, limit);
} catch (error) {
const message = `LSP workspace symbol search failed: ${
(error as Error)?.message || String(error)
}`;
return { llmContent: message, returnDisplay: message };
}
if (!symbols.length) {
const message = `No symbols found for query "${this.params.query}".`;
return { llmContent: message, returnDisplay: message };
}
const workspaceRoot = this.config.getProjectRoot();
const lines = symbols.slice(0, limit).map((symbol, index) => {
const location = this.formatLocation(symbol, workspaceRoot);
const serverSuffix = symbol.serverName
? ` [${symbol.serverName}]`
: '';
const kind = symbol.kind ? ` (${symbol.kind})` : '';
const container = symbol.containerName
? ` in ${symbol.containerName}`
: '';
return `${index + 1}. ${symbol.name}${kind}${container} - ${location}${serverSuffix}`;
});
const heading = `Found ${Math.min(symbols.length, limit)} of ${
symbols.length
} symbols for query "${this.params.query}":`;
let referenceSection = '';
const topSymbol = symbols[0];
if (topSymbol) {
try {
const referenceLimit = Math.min(20, Math.max(limit, 5));
const references = await client.references(
topSymbol.location,
topSymbol.serverName,
false,
referenceLimit,
);
if (references.length > 0) {
const refLines = references.map((ref, index) => {
const location = this.formatLocation(
{ location: ref, name: '', kind: undefined },
workspaceRoot,
);
const serverSuffix = ref.serverName
? ` [${ref.serverName}]`
: '';
return `${index + 1}. ${location}${serverSuffix}`;
});
referenceSection = [
'',
`References for top match (${topSymbol.name}):`,
...refLines,
].join('\n');
}
} catch (error) {
referenceSection = `\nReferences lookup failed: ${
(error as Error)?.message || String(error)
}`;
}
}
const llmParts = referenceSection
? [heading, ...lines, referenceSection]
: [heading, ...lines];
const displayParts = referenceSection
? [...lines, referenceSection]
: [...lines];
return {
llmContent: llmParts.join('\n'),
returnDisplay: displayParts.join('\n'),
};
}
private formatLocation(symbol: LspSymbolInformation, workspaceRoot: string) {
const { uri, range } = symbol.location;
let filePath = uri;
if (uri.startsWith('file://')) {
filePath = fileURLToPath(uri);
filePath = path.relative(workspaceRoot, filePath) || '.';
}
const line = (range.start.line ?? 0) + 1;
const character = (range.start.character ?? 0) + 1;
return `${filePath}:${line}:${character}`;
}
}
export class LspWorkspaceSymbolTool extends BaseDeclarativeTool<
LspWorkspaceSymbolParams,
ToolResult
> {
static readonly Name = ToolNames.LSP_WORKSPACE_SYMBOL;
constructor(private readonly config: Config) {
super(
LspWorkspaceSymbolTool.Name,
ToolDisplayNames.LSP_WORKSPACE_SYMBOL,
'Search workspace symbols via LSP查找定义/实现/引用,按名称定位符号,优先于 grep。',
Kind.Other,
{
type: 'object',
properties: {
query: {
type: 'string',
description:
'Symbol name query, e.g., function/class/variable name to search.',
},
limit: {
type: 'number',
description: 'Optional maximum number of results to return.',
},
},
required: ['query'],
},
false,
false,
);
}
protected createInvocation(
params: LspWorkspaceSymbolParams,
): ToolInvocation<LspWorkspaceSymbolParams, ToolResult> {
return new LspWorkspaceSymbolInvocation(this.config, params);
}
}

View File

@@ -248,7 +248,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir',
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -275,7 +275,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -300,7 +300,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -325,7 +325,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -350,7 +350,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir/subdir',
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -378,7 +378,7 @@ describe('ShellTool', () => {
'dir',
'/test/dir',
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -471,7 +471,7 @@ describe('ShellTool', () => {
expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith(
expect.any(String),
mockConfig.getGeminiClient(),
expect.any(AbortSignal),
mockAbortSignal,
1000,
);
expect(result.llmContent).toBe('summarized output');
@@ -580,7 +580,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -610,7 +610,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -640,7 +640,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -699,7 +699,7 @@ describe('ShellTool', () => {
expect.stringContaining('npm install'),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -728,7 +728,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit'),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -758,7 +758,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -794,7 +794,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit -m "Initial commit"'),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -831,7 +831,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
mockAbortSignal,
false,
{},
);
@@ -962,41 +962,4 @@ spanning multiple lines"`;
expect(shellTool.description).toMatchSnapshot();
});
});
describe('Windows background execution', () => {
it('should clean up trailing ampersand on Windows for background tasks', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const mockAbortSignal = new AbortController().signal;
const invocation = shellTool.build({
command: 'npm start &',
is_background: true,
});
const promise = invocation.execute(mockAbortSignal);
// Simulate immediate success (process started)
resolveExecutionPromise({
rawOutput: Buffer.from(''),
output: '',
exitCode: 0,
signal: null,
error: null,
aborted: false,
pid: 12345,
executionMethod: 'child_process',
});
await promise;
expect(mockShellExecutionService).toHaveBeenCalledWith(
'npm start',
expect.any(String),
expect.any(Function),
expect.any(AbortSignal),
false,
{},
);
});
});
});

View File

@@ -143,24 +143,11 @@ export class ShellToolInvocation extends BaseToolInvocation<
const shouldRunInBackground = this.params.is_background;
let finalCommand = processedCommand;
// On non-Windows, use & to run in background.
// On Windows, we don't use start /B because it creates a detached process that
// doesn't die when the parent dies. Instead, we rely on the race logic below
// to return early while keeping the process attached (detached: false).
if (
!isWindows &&
shouldRunInBackground &&
!finalCommand.trim().endsWith('&')
) {
// If explicitly marked as background and doesn't already end with &, add it
if (shouldRunInBackground && !finalCommand.trim().endsWith('&')) {
finalCommand = finalCommand.trim() + ' &';
}
// On Windows, we rely on the race logic below to handle background tasks.
// We just ensure the command string is clean.
if (isWindows && shouldRunInBackground) {
finalCommand = finalCommand.trim().replace(/&+$/, '').trim();
}
// pgrep is not available on Windows, so we can't get background PIDs
const commandToExecute = isWindows
? finalCommand
@@ -182,6 +169,10 @@ export class ShellToolInvocation extends BaseToolInvocation<
commandToExecute,
cwd,
(event: ShellOutputEvent) => {
if (!updateOutput) {
return;
}
let shouldUpdate = false;
switch (event.type) {
@@ -210,7 +201,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
}
if (shouldUpdate && updateOutput) {
if (shouldUpdate) {
updateOutput(
typeof cumulativeOutput === 'string'
? cumulativeOutput
@@ -228,21 +219,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
setPidCallback(pid);
}
if (shouldRunInBackground) {
// For background tasks, return immediately with PID info
// Note: We cannot reliably detect startup errors for background processes
// since their stdio is typically detached/ignored
const pidMsg = pid ? ` PID: ${pid}` : '';
const killHint = isWindows
? ' (Use taskkill /F /T /PID <pid> to stop)'
: ' (Use kill <pid> to stop)';
return {
llmContent: `Background command started.${pidMsg}${killHint}`,
returnDisplay: `Background command started.${pidMsg}${killHint}`,
};
}
const result = await resultPromise;
const backgroundPIDs: number[] = [];

View File

@@ -25,6 +25,9 @@ export const ToolNames = {
WEB_FETCH: 'web_fetch',
WEB_SEARCH: 'web_search',
LS: 'list_directory',
LSP_WORKSPACE_SYMBOL: 'lsp_workspace_symbol',
LSP_GO_TO_DEFINITION: 'lsp_go_to_definition',
LSP_FIND_REFERENCES: 'lsp_find_references',
} as const;
/**
@@ -48,6 +51,9 @@ export const ToolDisplayNames = {
WEB_FETCH: 'WebFetch',
WEB_SEARCH: 'WebSearch',
LS: 'ListFiles',
LSP_WORKSPACE_SYMBOL: 'LspWorkspaceSymbol',
LSP_GO_TO_DEFINITION: 'LspGoToDefinition',
LSP_FIND_REFERENCES: 'LspFindReferences',
} as const;
// Migration from old tool names to new tool names
@@ -56,6 +62,8 @@ export const ToolDisplayNames = {
export const ToolNamesMigration = {
search_file_content: ToolNames.GREP, // Legacy name from grep tool
replace: ToolNames.EDIT, // Legacy name from edit tool
go_to_definition: ToolNames.LSP_GO_TO_DEFINITION,
find_references: ToolNames.LSP_FIND_REFERENCES,
} as const;
// Migration from old tool display names to new tool display names

View File

@@ -1,24 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
tab_width = 4
ij_continuation_indent_size = 8
[*.java]
ij_java_doc_align_exception_comments = false
ij_java_doc_align_param_comments = false
[*.{yaml, yml, sh, ps1}]
indent_size = 2
[*.{md, mkd, markdown}]
trim_trailing_whitespace = false
[{**/res/**.xml, **/AndroidManifest.xml}]
ij_continuation_indent_size = 4

View File

@@ -1,14 +0,0 @@
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
# Mac
.DS_Store
# Maven
log/
target/
/docs/

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,378 +0,0 @@
# Qwen Code Java SDK
## Project Overview
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
**Context Information:**
- Current Date: Monday 5 January 2026
- Operating System: darwin
- Working Directory: /Users/weigeng/repos/qwen-code/packages/sdk-java
## Project Details
- **Group ID**: com.alibaba
- **Artifact ID**: qwencode-sdk (as per pom.xml)
- **Version**: 0.0.1-SNAPSHOT
- **Packaging**: JAR
- **Java Version**: 1.8+ (source and target)
- **License**: Apache-2.0
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Components
### Main Classes
- `QwenCodeCli`: Main entry point with static methods for simple queries
- `Session`: Manages communication sessions with the CLI
- `Transport`: Abstracts the communication mechanism (currently using process transport)
- `ProcessTransport`: Implementation that communicates via process execution
- `TransportOptions`: Configuration class for transport layer settings
- `SessionEventSimpleConsumers`: High-level event handler for processing responses
- `AssistantContentSimpleConsumers`: Handles different types of content within assistant messages
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Building and Running
### Prerequisites
- Java 8 or higher
- Apache Maven 3.6.0 or higher
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
# Run checkstyle verification
mvn checkstyle:check
# Generate Javadoc
mvn javadoc:javadoc
```
### Testing
The project includes basic unit tests using JUnit 5. The main test class `QwenCodeCliTest` demonstrates how to use the SDK to make simple queries to the Qwen Code CLI.
### Code Quality
The project uses Checkstyle for code formatting and style enforcement. The configuration is defined in `checkstyle.xml` and includes rules for:
- Whitespace and indentation
- Naming conventions
- Import ordering
- Code structure
- Line endings (LF only)
- No trailing whitespace
- 8-space indentation for line wrapping
## Development Conventions
### Coding Standards
- Java 8 language features are supported
- Follow standard Java naming conventions
- Use UTF-8 encoding for source files
- Line endings should be LF (Unix-style)
- No trailing whitespace allowed
- Use 8-space indentation for line wrapping
### Testing Practices
- Write unit tests using JUnit 5
- Test classes should be in the `src/test/java` directory
- Follow the naming convention `*Test.java` for test classes
- Use appropriate assertions to validate functionality
### Documentation
- API documentation should follow Javadoc conventions
- Update README files when adding new features
- Include examples in documentation
## API Reference
### QwenCodeCli Class
The main class provides several primary methods:
- `simpleQuery(String prompt)`: Synchronous method that returns a list of responses
- `simpleQuery(String prompt, TransportOptions transportOptions)`: Synchronous method with custom transport options
- `simpleQuery(String prompt, TransportOptions transportOptions, AssistantContentConsumers assistantContentConsumers)`: Advanced method with custom content consumers
- `newSession()`: Creates a new session with default options
- `newSession(TransportOptions transportOptions)`: Creates a new session with custom options
### Permission Modes
The SDK supports different permission modes for controlling tool execution:
- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.
### Transport Options
The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:
- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: Additional command-line options to pass to the CLI
### Session Control Features
- **Session creation**: Use `QwenCodeCli.newSession()` to create a new session with custom options
- **Session management**: The `Session` class provides methods to send prompts, handle responses, and manage session state
- **Session cleanup**: Always close sessions using `session.close()` to properly terminate the CLI process
- **Session resumption**: Use `setResumeSessionId()` in `TransportOptions` to resume a previous session
- **Session interruption**: Use `session.interrupt()` to interrupt a currently running prompt
- **Dynamic model switching**: Use `session.setModel()` to change the model during a session
- **Dynamic permission mode switching**: Use `session.setPermissionMode()` to change the permission mode during a session
### Thread Pool Configuration
The SDK uses a thread pool for managing concurrent operations with the following default configuration:
- **Core Pool Size**: 30 threads
- **Maximum Pool Size**: 100 threads
- **Keep-Alive Time**: 60 seconds
- **Queue Capacity**: 300 tasks (using LinkedBlockingQueue)
- **Thread Naming**: "qwen_code_cli-pool-{number}"
- **Daemon Threads**: false
- **Rejected Execution Handler**: CallerRunsPolicy
### Session Event Consumers and Assistant Content Consumers
The SDK provides two key interfaces for handling events and content from the CLI:
#### SessionEventConsumers Interface
The `SessionEventConsumers` interface provides callbacks for different types of messages during a session:
- `onSystemMessage`: Handles system messages from the CLI (receives Session and SDKSystemMessage)
- `onResultMessage`: Handles result messages from the CLI (receives Session and SDKResultMessage)
- `onAssistantMessage`: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)
- `onPartialAssistantMessage`: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)
- `onUserMessage`: Handles user messages (receives Session and SDKUserMessage)
- `onOtherMessage`: Handles other types of messages (receives Session and String message)
- `onControlResponse`: Handles control responses (receives Session and CLIControlResponse)
- `onControlRequest`: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)
#### AssistantContentConsumers Interface
The `AssistantContentConsumers` interface handles different types of content within assistant messages:
- `onText`: Handles text content (receives Session and TextAssistantContent)
- `onThinking`: Handles thinking content (receives Session and ThingkingAssistantContent)
- `onToolUse`: Handles tool use content (receives Session and ToolUseAssistantContent)
- `onToolResult`: Handles tool result content (receives Session and ToolResultAssistantContent)
- `onOtherContent`: Handles other content types (receives Session and AssistantContent)
- `onUsage`: Handles usage information (receives Session and AssistantUsage)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)
- `onOtherControlRequest`: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)
#### Relationship Between the Interfaces
**Important Note on Event Hierarchy:**
- `SessionEventConsumers` is the **high-level** event processor that handles different message types (system, assistant, user, etc.)
- `AssistantContentConsumers` is the **low-level** content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)
**Processor Relationship:**
- `SessionEventConsumers``AssistantContentConsumers` (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)
**Event Derivation Relationships:**
- `onAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``onPermissionRequest`, `onOtherControlRequest`
**Event Timeout Relationships:**
Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:
- `onSystemMessage``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default Timeout Values:**
- `SessionEventSimpleConsumers` default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)
- `AssistantContentSimpleConsumers` default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)
**Timeout Hierarchy Requirements:**
For proper operation, the following timeout relationships should be maintained:
- `onAssistantMessageTimeout` return value should be greater than `onTextTimeout`, `onThinkingTimeout`, `onToolUseTimeout`, `onToolResultTimeout`, and `onOtherContentTimeout` return values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values
#### Relationship Between the Interfaces
- `AssistantContentSimpleConsumers` is the default implementation of `AssistantContentConsumers`
- `SessionEventSimpleConsumers` is the concrete implementation that combines both interfaces and depends on an `AssistantContentConsumers` instance to handle content within assistant messages
- The timeout methods in `SessionEventConsumers` now include the message object as a parameter (e.g., `onSystemMessageTimeout(Session session, SDKSystemMessage systemMessage)`)
Event processing is subject to the timeout settings configured in `TransportOptions` and `SessionEventConsumers`. For detailed timeout configuration options, see the "Timeout" section above.
## Usage Examples
The SDK includes several example files in `src/test/java/com/alibaba/qwen/code/cli/example/` that demonstrate different aspects of the API:
### Basic Usage
- `QuickStartExample.java`: Demonstrates simple query usage, transport options configuration, and streaming content handling
### Session Control
- `SessionExample.java`: Shows session control features including permission mode changes, model switching, interruption, and event handling
### Configuration
- `ThreadPoolConfigurationExample.java`: Shows how to configure the thread pool used by the SDK
## Error Handling
The SDK provides specific exception types for different error scenarios:
- `SessionControlException`: Thrown when there's an issue with session control (creation, initialization, etc.)
- `SessionSendPromptException`: Thrown when there's an issue sending a prompt or receiving a response
- `SessionClosedException`: Thrown when attempting to use a closed session
## Project Structure
```
src/
├── example/
│ └── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── example/
├── main/
│ └── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── cli/
│ ├── QwenCodeCli.java
│ ├── protocol/
│ ├── session/
│ ├── transport/
│ └── utils/
└── test/
├── java/
│ └── com/
│ └── alibaba/
│ └── qwen/
│ └── code/
│ └── cli/
│ ├── QwenCodeCliTest.java
│ ├── session/
│ │ └── SessionTest.java
│ └── transport/
│ ├── PermissionModeTest.java
│ └── process/
│ └── ProcessTransportTest.java
└── temp/
```
## Configuration Files
- `pom.xml`: Maven build configuration and dependencies
- `checkstyle.xml`: Code style and formatting rules
- `.editorconfig`: Editor configuration settings
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: No, from v0.1.1, the CLI is bundled with the SDK, so no standalone CLI installation is needed.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### Q: How do I handle long-running requests?
A: The SDK includes timeout utilities. You can configure timeouts using the `Timeout` class in `TransportOptions`.
### Q: Why are some tools not executing?
A: This is likely due to permission modes. Check your permission mode settings and consider using `allowedTools` to pre-approve certain tools.
### Q: How do I resume a previous session?
A: Use the `setResumeSessionId()` method in `TransportOptions` to resume a previous session.
### Q: Can I customize the environment for the CLI process?
A: Yes, use the `setEnv()` method in `TransportOptions` to pass environment variables to the CLI process.
### Q: What happens if the CLI process crashes?
A: The SDK will throw appropriate exceptions. Make sure to handle `SessionControlException` and implement retry logic if needed.
## Maintainers
- **Developer**: skyfire (gengwei.gw(at)alibaba-inc.com)
- **Organization**: Alibaba Group

View File

@@ -1,312 +0,0 @@
# Qwen Code Java SDK
The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
## Requirements
- Java >= 1.8
- Maven >= 3.6.0 (for building from source)
- qwen-code >= 0.5.0
### Dependencies
- **Logging**: ch.qos.logback:logback-classic
- **Utilities**: org.apache.commons:commons-lang3
- **JSON Processing**: com.alibaba.fastjson2:fastjson2
- **Testing**: JUnit 5 (org.junit.jupiter:junit-jupiter)
## Installation
Add the following dependency to your Maven `pom.xml`:
```xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<version>{$version}</version>
</dependency>
```
Or if using Gradle, add to your `build.gradle`:
```gradle
implementation 'com.alibaba:qwencode-sdk:{$version}'
```
## Building and Running
### Build Commands
```bash
# Compile the project
mvn compile
# Run tests
mvn test
# Package the JAR
mvn package
# Install to local repository
mvn install
```
## Quick Start
The simplest way to use the SDK is through the `QwenCodeCli.simpleQuery()` method:
```java
public static void runSimpleExample() {
List<String> result = QwenCodeCli.simpleQuery("hello world");
result.forEach(logger::info);
}
```
For more advanced usage with custom transport options:
```java
public static void runTransportOptionsExample() {
TransportOptions options = new TransportOptions()
.setModel("qwen3-coder-flash")
.setPermissionMode(PermissionMode.AUTO_EDIT)
.setCwd("./")
.setEnv(new HashMap<String, String>() {{put("CUSTOM_VAR", "value");}})
.setIncludePartialMessages(true)
.setTurnTimeout(new Timeout(120L, TimeUnit.SECONDS))
.setMessageTimeout(new Timeout(90L, TimeUnit.SECONDS))
.setAllowedTools(Arrays.asList("read_file", "write_file", "list_directory"));
List<String> result = QwenCodeCli.simpleQuery("who are you, what are your capabilities?", options);
result.forEach(logger::info);
}
```
For streaming content handling with custom content consumers:
```java
public static void runStreamingExample() {
QwenCodeCli.simpleQuery("who are you, what are your capabilities?",
new TransportOptions().setMessageTimeout(new Timeout(10L, TimeUnit.SECONDS)), new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
logger.info("Text content received: {}", textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
logger.info("Thinking content received: {}", thingkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseContent) {
logger.info("Tool use content received: {} with arguments: {}",
toolUseContent, toolUseContent.getInput());
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultContent) {
logger.info("Tool result content received: {}", toolResultContent.getContent());
}
@Override
public void onOtherContent(Session session, AssistantContent<?> other) {
logger.info("Other content received: {}", other);
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
logger.info("Usage information received: Input tokens: {}, Output tokens: {}",
assistantUsage.getUsage().getInputTokens(), assistantUsage.getUsage().getOutputTokens());
}
}.setDefaultPermissionOperation(Operation.allow));
logger.info("Streaming example completed.");
}
```
other examples see src/test/java/com/alibaba/qwen/code/cli/example
## Architecture
The SDK follows a layered architecture:
- **API Layer**: Provides the main entry points through `QwenCodeCli` class with simple static methods for basic usage
- **Session Layer**: Manages communication sessions with the Qwen Code CLI through the `Session` class
- **Transport Layer**: Handles the communication mechanism between the SDK and CLI process (currently using process transport via `ProcessTransport`)
- **Protocol Layer**: Defines data structures for communication based on the CLI protocol
- **Utils**: Common utilities for concurrent execution, timeout handling, and error management
## Key Features
### Permission Modes
The SDK supports different permission modes for controlling tool execution:
- **`default`**: Write tools are denied unless approved via `canUseTool` callback or in `allowedTools`. Read-only tools execute without confirmation.
- **`plan`**: Blocks all write tools, instructing AI to present a plan first.
- **`auto-edit`**: Auto-approve edit tools (edit, write_file) while other tools require confirmation.
- **`yolo`**: All tools execute automatically without confirmation.
### Session Event Consumers and Assistant Content Consumers
The SDK provides two key interfaces for handling events and content from the CLI:
#### SessionEventConsumers Interface
The `SessionEventConsumers` interface provides callbacks for different types of messages during a session:
- `onSystemMessage`: Handles system messages from the CLI (receives Session and SDKSystemMessage)
- `onResultMessage`: Handles result messages from the CLI (receives Session and SDKResultMessage)
- `onAssistantMessage`: Handles assistant messages (AI responses) (receives Session and SDKAssistantMessage)
- `onPartialAssistantMessage`: Handles partial assistant messages during streaming (receives Session and SDKPartialAssistantMessage)
- `onUserMessage`: Handles user messages (receives Session and SDKUserMessage)
- `onOtherMessage`: Handles other types of messages (receives Session and String message)
- `onControlResponse`: Handles control responses (receives Session and CLIControlResponse)
- `onControlRequest`: Handles control requests (receives Session and CLIControlRequest, returns CLIControlResponse)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlRequest<CLIControlPermissionRequest>, returns Behavior)
#### AssistantContentConsumers Interface
The `AssistantContentConsumers` interface handles different types of content within assistant messages:
- `onText`: Handles text content (receives Session and TextAssistantContent)
- `onThinking`: Handles thinking content (receives Session and ThingkingAssistantContent)
- `onToolUse`: Handles tool use content (receives Session and ToolUseAssistantContent)
- `onToolResult`: Handles tool result content (receives Session and ToolResultAssistantContent)
- `onOtherContent`: Handles other content types (receives Session and AssistantContent)
- `onUsage`: Handles usage information (receives Session and AssistantUsage)
- `onPermissionRequest`: Handles permission requests (receives Session and CLIControlPermissionRequest, returns Behavior)
- `onOtherControlRequest`: Handles other control requests (receives Session and ControlRequestPayload, returns ControlResponsePayload)
#### Relationship Between the Interfaces
**Important Note on Event Hierarchy:**
- `SessionEventConsumers` is the **high-level** event processor that handles different message types (system, assistant, user, etc.)
- `AssistantContentConsumers` is the **low-level** content processor that handles different types of content within assistant messages (text, tools, thinking, etc.)
**Processor Relationship:**
- `SessionEventConsumers``AssistantContentConsumers` (SessionEventConsumers uses AssistantContentConsumers to process content within assistant messages)
**Event Derivation Relationships:**
- `onAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`, `onUsage`
- `onPartialAssistantMessage``onText`, `onThinking`, `onToolUse`, `onToolResult`, `onOtherContent`
- `onControlRequest``onPermissionRequest`, `onOtherControlRequest`
**Event Timeout Relationships:**
Each event handler method has a corresponding timeout method that allows customizing the timeout behavior for that specific event:
- `onSystemMessage``onSystemMessageTimeout`
- `onResultMessage``onResultMessageTimeout`
- `onAssistantMessage``onAssistantMessageTimeout`
- `onPartialAssistantMessage``onPartialAssistantMessageTimeout`
- `onUserMessage``onUserMessageTimeout`
- `onOtherMessage``onOtherMessageTimeout`
- `onControlResponse``onControlResponseTimeout`
- `onControlRequest``onControlRequestTimeout`
For AssistantContentConsumers timeout methods:
- `onText``onTextTimeout`
- `onThinking``onThinkingTimeout`
- `onToolUse``onToolUseTimeout`
- `onToolResult``onToolResultTimeout`
- `onOtherContent``onOtherContentTimeout`
- `onPermissionRequest``onPermissionRequestTimeout`
- `onOtherControlRequest``onOtherControlRequestTimeout`
**Default Timeout Values:**
- `SessionEventSimpleConsumers` default timeout: 180 seconds (Timeout.TIMEOUT_180_SECONDS)
- `AssistantContentSimpleConsumers` default timeout: 60 seconds (Timeout.TIMEOUT_60_SECONDS)
**Timeout Hierarchy Requirements:**
For proper operation, the following timeout relationships should be maintained:
- `onAssistantMessageTimeout` return value should be greater than `onTextTimeout`, `onThinkingTimeout`, `onToolUseTimeout`, `onToolResultTimeout`, and `onOtherContentTimeout` return values
- `onControlRequestTimeout` return value should be greater than `onPermissionRequestTimeout` and `onOtherControlRequestTimeout` return values
### Transport Options
The `TransportOptions` class allows configuration of how the SDK communicates with the Qwen Code CLI:
- `pathToQwenExecutable`: Path to the Qwen Code CLI executable
- `cwd`: Working directory for the CLI process
- `model`: AI model to use for the session
- `permissionMode`: Permission mode that controls tool execution
- `env`: Environment variables to pass to the CLI process
- `maxSessionTurns`: Limits the number of conversation turns in a session
- `coreTools`: List of core tools that should be available to the AI
- `excludeTools`: List of tools to exclude from being available to the AI
- `allowedTools`: List of tools that are pre-approved for use without additional confirmation
- `authType`: Authentication type to use for the session
- `includePartialMessages`: Enables receiving partial messages during streaming responses
- `skillsEnable`: Enables or disables skills functionality for the session
- `turnTimeout`: Timeout for a complete turn of conversation
- `messageTimeout`: Timeout for individual messages within a turn
- `resumeSessionId`: ID of a previous session to resume
- `otherOptions`: Additional command-line options to pass to the CLI
### Session Control Features
- **Session creation**: Use `QwenCodeCli.newSession()` to create a new session with custom options
- **Session management**: The `Session` class provides methods to send prompts, handle responses, and manage session state
- **Session cleanup**: Always close sessions using `session.close()` to properly terminate the CLI process
- **Session resumption**: Use `setResumeSessionId()` in `TransportOptions` to resume a previous session
- **Session interruption**: Use `session.interrupt()` to interrupt a currently running prompt
- **Dynamic model switching**: Use `session.setModel()` to change the model during a session
- **Dynamic permission mode switching**: Use `session.setPermissionMode()` to change the permission mode during a session
### Thread Pool Configuration
The SDK uses a thread pool for managing concurrent operations with the following default configuration:
- **Core Pool Size**: 30 threads
- **Maximum Pool Size**: 100 threads
- **Keep-Alive Time**: 60 seconds
- **Queue Capacity**: 300 tasks (using LinkedBlockingQueue)
- **Thread Naming**: "qwen_code_cli-pool-{number}"
- **Daemon Threads**: false
- **Rejected Execution Handler**: CallerRunsPolicy
## Error Handling
The SDK provides specific exception types for different error scenarios:
- `SessionControlException`: Thrown when there's an issue with session control (creation, initialization, etc.)
- `SessionSendPromptException`: Thrown when there's an issue sending a prompt or receiving a response
- `SessionClosedException`: Thrown when attempting to use a closed session
## FAQ / Troubleshooting
### Q: Do I need to install the Qwen CLI separately?
A: No, from v0.1.1, the CLI is bundled with the SDK, so no standalone CLI installation is needed.
### Q: What Java versions are supported?
A: The SDK requires Java 1.8 or higher.
### Q: How do I handle long-running requests?
A: The SDK includes timeout utilities. You can configure timeouts using the `Timeout` class in `TransportOptions`.
### Q: Why are some tools not executing?
A: This is likely due to permission modes. Check your permission mode settings and consider using `allowedTools` to pre-approve certain tools.
### Q: How do I resume a previous session?
A: Use the `setResumeSessionId()` method in `TransportOptions` to resume a previous session.
### Q: Can I customize the environment for the CLI process?
A: Yes, use the `setEnv()` method in `TransportOptions` to pass environment variables to the CLI process.
## License
Apache-2.0 - see [LICENSE](./LICENSE) for details.

View File

@@ -1,131 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="FileTabCharacter" />
<module name="NewlineAtEndOfFile">
<property name="lineSeparator" value="lf" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\r" />
<property name="message" value="Line contains carriage return" />
</module>
<module name="RegexpMultiline">
<property name="format" value=" \n" />
<property name="message" value="Line has trailing whitespace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\n" />
<property name="message" value="Multiple consecutive blank lines" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\Z" />
<property name="message" value="Blank line before end of file" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\{\n\n" />
<property name="message" value="Blank line after opening brace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="\n\n\s*\}" />
<property name="message" value="Blank line before closing brace" />
</module>
<module name="RegexpMultiline">
<property name="format" value="->\s*\{\s+\}" />
<property name="message" value="Whitespace inside empty lambda body" />
</module>
<module name="TreeWalker">
<module name="SuppressWarningsHolder" />
<module name="EmptyBlock">
<property name="option" value="text" />
<property name="tokens" value="
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_IF,
LITERAL_FOR, LITERAL_TRY, LITERAL_WHILE, INSTANCE_INIT, STATIC_INIT" />
</module>
<module name="EmptyStatement" />
<module name="EmptyForInitializerPad" />
<module name="MethodParamPad">
<property name="allowLineBreaks" value="true" />
<property name="option" value="nospace" />
</module>
<module name="ParenPad" />
<module name="TypecastParenPad" />
<module name="NeedBraces" />
<module name="LeftCurly">
<property name="option" value="eol" />
<property name="tokens" value="
LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR,
LITERAL_IF, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE" />
</module>
<module name="GenericWhitespace" />
<module name="WhitespaceAfter" />
<module name="NoWhitespaceAfter" />
<module name="NoWhitespaceBefore" />
<module name="SingleSpaceSeparator" />
<module name="Indentation">
<property name="throwsIndent" value="8" />
<property name="lineWrappingIndentation" value="8" />
</module>
<module name="UpperEll" />
<module name="DefaultComesLast" />
<module name="ArrayTypeStyle" />
<module name="ModifierOrder" />
<module name="OneStatementPerLine" />
<module name="StringLiteralEquality" />
<module name="MutableException" />
<module name="EqualsHashCode" />
<module name="ExplicitInitialization" />
<module name="OneTopLevelClass" />
<module name="MemberName" />
<module name="PackageName" />
<module name="ClassTypeParameterName">
<property name="format" value="^[A-Z][0-9]?$" />
</module>
<module name="MethodTypeParameterName">
<property name="format" value="^[A-Z][0-9]?$" />
</module>
<module name="AnnotationUseStyle">
<property name="trailingArrayComma" value="ignore" />
</module>
<module name="RedundantImport" />
<module name="UnusedImports" />
<!-- <module name="ImportOrder">-->
<!-- <property name="groups" value="*,javax,java" />-->
<!-- <property name="separated" value="true" />-->
<!-- <property name="option" value="bottom" />-->
<!-- <property name="sortStaticImportsAlphabetically" value="true" />-->
<!-- </module>-->
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true" />
<property name="allowEmptyMethods" value="true" />
<property name="allowEmptyLambdas" value="true" />
<property name="ignoreEnhancedForColon" value="false" />
<property name="tokens" value="
ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN,
BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND,
LAMBDA, LE, LITERAL_ASSERT, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE,
LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH,
LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE,
LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL,
PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN,
STAR, STAR_ASSIGN, TYPE_EXTENSION_AND" />
</module>
<module name="WhitespaceAfter" />
<module name="NoWhitespaceAfter">
<property name="tokens" value="DOT" />
<property name="allowLineBreaks" value="false" />
</module>
<module name="MissingOverride"/>
</module>
</module>

View File

@@ -1,193 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
<packaging>jar</packaging>
<version>0.0.1-alpha</version>
<name>qwencode-sdk</name>
<description>The Qwen Code Java SDK is a minimum experimental SDK for programmatic access to Qwen Code functionality. It provides a Java interface
to interact with the Qwen Code CLI, allowing developers to integrate Qwen Code capabilities into their Java applications.
</description>
<url>https://maven.apache.org</url>
<licenses>
<license>
<name>Apache 2</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
<comments>A business-friendly OSS license</comments>
</license>
</licenses>
<scm>
<url>https://github.com/QwenLM/qwen-code</url>
<connection>scm:git:https://github.com/QwenLM/qwen-code.git</connection>
</scm>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<checkstyle-maven-plugin.version>3.6.0</checkstyle-maven-plugin.version>
<jacoco-maven-plugin.version>0.8.12</jacoco-maven-plugin.version>
<junit5.version>5.14.1</junit5.version>
<logback-classic.version>1.3.16</logback-classic.version>
<fastjson2.version>2.0.60</fastjson2.version>
<maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
<central-publishing-maven-plugin.version>0.8.0</central-publishing-maven-plugin.version>
<maven-source-plugin.version>2.2.1</maven-source-plugin.version>
<maven-javadoc-plugin.version>2.9.1</maven-javadoc-plugin.version>
<maven-gpg-plugin.version>1.5</maven-gpg-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<type>pom</type>
<version>${junit5.version}</version>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback-classic.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-maven-plugin.version}</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>${central-publishing-maven-plugin.version}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven-source-plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<organization>
<name>Alibaba Group</name>
<url>https://github.com/alibaba</url>
</organization>
<developers>
<developer>
<id>skyfire</id>
<name>skyfire</name>
<email>gengwei.gw(at)alibaba-inc.com</email>
<roles>
<role>Developer</role>
<role>Designer</role>
</roles>
<timezone>+8</timezone>
<url>https://github.com/gwinthis</url>
</developer>
</developers>
<distributionManagement>
<snapshotRepository>
<id>central</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
</snapshotRepository>
<repository>
<id>central</id>
<url>https://central.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</project>

View File

@@ -1,142 +0,0 @@
package com.alibaba.qwen.code.cli;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import com.alibaba.qwen.code.cli.protocol.data.AssistantUsage;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.behavior.Behavior.Operation;
import com.alibaba.qwen.code.cli.session.Session;
import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentConsumers;
import com.alibaba.qwen.code.cli.session.event.consumers.AssistantContentSimpleConsumers;
import com.alibaba.qwen.code.cli.session.event.consumers.SessionEventSimpleConsumers;
import com.alibaba.qwen.code.cli.transport.Transport;
import com.alibaba.qwen.code.cli.transport.TransportOptions;
import com.alibaba.qwen.code.cli.transport.process.ProcessTransport;
import com.alibaba.qwen.code.cli.utils.MyConcurrentUtils;
import com.alibaba.qwen.code.cli.utils.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main entry point for interacting with the Qwen Code CLI. Provides static methods for simple queries and session management.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class QwenCodeCli {
private static final Logger log = LoggerFactory.getLogger(QwenCodeCli.class);
/**
* Sends a simple query to the Qwen Code CLI and returns a list of responses.
*
* @param prompt The input prompt to send to the CLI
* @return A list of strings representing the CLI's responses
*/
public static List<String> simpleQuery(String prompt) {
return simpleQuery(prompt, new TransportOptions());
}
/**
* Sends a simple query with custom transport options.
*
* @param prompt The input prompt to send to the CLI
* @param transportOptions Configuration options for the transport layer
* @return A list of strings representing the CLI's responses
*/
public static List<String> simpleQuery(String prompt, TransportOptions transportOptions) {
final List<String> response = new ArrayList<>();
MyConcurrentUtils.runAndWait(() -> simpleQuery(prompt, transportOptions, new AssistantContentSimpleConsumers() {
@Override
public void onText(Session session, TextAssistantContent textAssistantContent) {
response.add(textAssistantContent.getText());
}
@Override
public void onThinking(Session session, ThingkingAssistantContent thingkingAssistantContent) {
response.add(thingkingAssistantContent.getThinking());
}
@Override
public void onToolUse(Session session, ToolUseAssistantContent toolUseAssistantContent) {
response.add(JSON.toJSONString(toolUseAssistantContent.getContentOfAssistant()));
}
@Override
public void onToolResult(Session session, ToolResultAssistantContent toolResultAssistantContent) {
response.add(JSON.toJSONString(toolResultAssistantContent));
}
public void onOtherContent(Session session, AssistantContent<?> other) {
response.add(JSON.toJSONString(other.getContentOfAssistant()));
}
@Override
public void onUsage(Session session, AssistantUsage assistantUsage) {
log.info("received usage {} of message {}", assistantUsage.getUsage(), assistantUsage.getMessageId());
}
}.setDefaultPermissionOperation(Operation.allow)), Timeout.TIMEOUT_30_MINUTES);
return response;
}
/**
* Sends a query with custom content consumers.
*
* @param prompt The input prompt to send to the CLI
* @param transportOptions Configuration options for the transport layer
* @param assistantContentConsumers Consumers for handling different types of assistant content
*/
public static void simpleQuery(String prompt, TransportOptions transportOptions, AssistantContentConsumers assistantContentConsumers) {
Session session = newSession(transportOptions);
try {
session.sendPrompt(prompt, new SessionEventSimpleConsumers()
.setAssistantContentConsumer(assistantContentConsumers));
} catch (Exception e) {
throw new RuntimeException("sendPrompt error!", e);
} finally {
try {
session.close();
} catch (Exception e) {
log.error("close session error!", e);
}
}
}
/**
* Creates a new session with default transport options.
*
* @return A new Session instance
*/
public static Session newSession() {
return newSession(new TransportOptions());
}
/**
* Creates a new session with custom transport options.
*
* @param transportOptions Configuration options for the transport layer
* @return A new Session instance
*/
public static Session newSession(TransportOptions transportOptions) {
Transport transport;
try {
transport = new ProcessTransport(transportOptions);
} catch (Exception e) {
throw new RuntimeException("initialized ProcessTransport error!", e);
}
Session session;
try {
session = new Session(transport);
} catch (Exception e) {
throw new RuntimeException("initialized Session error!", e);
}
return session;
}
}

View File

@@ -1,95 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import java.util.Map;
/**
* Represents content from the assistant in a Qwen Code session.
*
* @param <C> The type of content
* @author skyfire
* @version $Id: 0.0.1
*/
public interface AssistantContent<C> {
/**
* Gets the type of the assistant content.
*
* @return The type of the assistant content
*/
String getType();
/**
* Gets the actual content from the assistant.
*
* @return The content from the assistant
*/
C getContentOfAssistant();
/**
* Gets the message ID associated with this content.
*
* @return The message ID
*/
String getMessageId();
/**
* Represents text content from the assistant.
*/
interface TextAssistantContent extends AssistantContent<String> {
/**
* Gets the text content.
*
* @return The text content
*/
String getText();
}
/**
* Represents thinking content from the assistant.
*/
interface ThingkingAssistantContent extends AssistantContent<String> {
/**
* Gets the thinking content.
*
* @return The thinking content
*/
String getThinking();
}
/**
* Represents tool use content from the assistant.
*/
interface ToolUseAssistantContent extends AssistantContent<Map<String, Object>> {
/**
* Gets the tool input.
*
* @return The tool input
*/
Map<String, Object> getInput();
}
/**
* Represents tool result content from the assistant.
*/
interface ToolResultAssistantContent extends AssistantContent<String> {
/**
* Gets whether the tool result indicates an error.
*
* @return Whether the tool result indicates an error
*/
Boolean getIsError();
/**
* Gets the tool result content.
*
* @return The tool result content
*/
String getContent();
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
String getToolUseId();
}
}

View File

@@ -1,76 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.JSON;
/**
* Represents usage information for an assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class AssistantUsage {
/**
* The ID of the message.
*/
String messageId;
/**
* The usage information.
*/
Usage usage;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public Usage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(Usage usage) {
this.usage = usage;
}
/**
* Constructs a new AssistantUsage instance.
*
* @param messageId The message ID
* @param usage The usage information
*/
public AssistantUsage(String messageId, Usage usage) {
this.messageId = messageId;
this.usage = usage;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -1,83 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents a permission denial from the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class CLIPermissionDenial {
/**
* The name of the denied tool.
*/
@JSONField(name = "tool_name")
private String toolName;
/**
* The ID of the denied tool use.
*/
@JSONField(name = "tool_use_id")
private String toolUseId;
/**
* The input for the denied tool.
*/
@JSONField(name = "tool_input")
private Object toolInput;
/**
* Gets the name of the denied tool.
*
* @return The name of the denied tool
*/
public String getToolName() {
return toolName;
}
/**
* Sets the name of the denied tool.
*
* @param toolName The name of the denied tool
*/
public void setToolName(String toolName) {
this.toolName = toolName;
}
/**
* Gets the ID of the denied tool use.
*
* @return The ID of the denied tool use
*/
public String getToolUseId() {
return toolUseId;
}
/**
* Sets the ID of the denied tool use.
*
* @param toolUseId The ID of the denied tool use
*/
public void setToolUseId(String toolUseId) {
this.toolUseId = toolUseId;
}
/**
* Gets the input for the denied tool.
*
* @return The input for the denied tool
*/
public Object getToolInput() {
return toolInput;
}
/**
* Sets the input for the denied tool.
*
* @param toolInput The input for the denied tool
*/
public void setToolInput(Object toolInput) {
this.toolInput = toolInput;
}
}

View File

@@ -1,131 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents the capabilities of the Qwen Code CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Capabilities {
/**
* Whether the CLI can handle can_use_tool requests.
*/
@JSONField(name = "can_handle_can_use_tool")
boolean canHandleCanUseTool;
/**
* Whether the CLI can handle hook callbacks.
*/
@JSONField(name = "can_handle_hook_callback")
boolean canHandleHookCallback;
/**
* Whether the CLI can set permission mode.
*/
@JSONField(name = "can_set_permission_mode")
boolean canSetPermissionMode;
/**
* Whether the CLI can set the model.
*/
@JSONField(name = "can_set_model")
boolean canSetModel;
/**
* Whether the CLI can handle MCP messages.
*/
@JSONField(name = "can_handle_mcp_message")
boolean canHandleMcpMessage;
/**
* Checks if the CLI can handle can_use_tool requests.
*
* @return true if the CLI can handle can_use_tool requests, false otherwise
*/
public boolean isCanHandleCanUseTool() {
return canHandleCanUseTool;
}
/**
* Sets whether the CLI can handle can_use_tool requests.
*
* @param canHandleCanUseTool Whether the CLI can handle can_use_tool requests
*/
public void setCanHandleCanUseTool(boolean canHandleCanUseTool) {
this.canHandleCanUseTool = canHandleCanUseTool;
}
/**
* Checks if the CLI can handle hook callbacks.
*
* @return true if the CLI can handle hook callbacks, false otherwise
*/
public boolean isCanHandleHookCallback() {
return canHandleHookCallback;
}
/**
* Sets whether the CLI can handle hook callbacks.
*
* @param canHandleHookCallback Whether the CLI can handle hook callbacks
*/
public void setCanHandleHookCallback(boolean canHandleHookCallback) {
this.canHandleHookCallback = canHandleHookCallback;
}
/**
* Checks if the CLI can set permission mode.
*
* @return true if the CLI can set permission mode, false otherwise
*/
public boolean isCanSetPermissionMode() {
return canSetPermissionMode;
}
/**
* Sets whether the CLI can set permission mode.
*
* @param canSetPermissionMode Whether the CLI can set permission mode
*/
public void setCanSetPermissionMode(boolean canSetPermissionMode) {
this.canSetPermissionMode = canSetPermissionMode;
}
/**
* Checks if the CLI can set the model.
*
* @return true if the CLI can set the model, false otherwise
*/
public boolean isCanSetModel() {
return canSetModel;
}
/**
* Sets whether the CLI can set the model.
*
* @param canSetModel Whether the CLI can set the model
*/
public void setCanSetModel(boolean canSetModel) {
this.canSetModel = canSetModel;
}
/**
* Checks if the CLI can handle MCP messages.
*
* @return true if the CLI can handle MCP messages, false otherwise
*/
public boolean isCanHandleMcpMessage() {
return canHandleMcpMessage;
}
/**
* Sets whether the CLI can handle MCP messages.
*
* @param canHandleMcpMessage Whether the CLI can handle MCP messages
*/
public void setCanHandleMcpMessage(boolean canHandleMcpMessage) {
this.canHandleMcpMessage = canHandleMcpMessage;
}
}

View File

@@ -1,147 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Extends the Usage class with additional usage information.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class ExtendedUsage extends Usage {
/**
* Server tool use information.
*/
@JSONField(name = "server_tool_use")
private ServerToolUse serverToolUse;
/**
* Service tier information.
*/
@JSONField(name = "service_tier")
private String serviceTier;
/**
* Cache creation information.
*/
@JSONField(name = "cache_creation")
private CacheCreation cacheCreation;
/**
* Gets the server tool use information.
*
* @return The server tool use information
*/
public ServerToolUse getServerToolUse() {
return serverToolUse;
}
/**
* Sets the server tool use information.
*
* @param serverToolUse The server tool use information
*/
public void setServerToolUse(ServerToolUse serverToolUse) {
this.serverToolUse = serverToolUse;
}
/**
* Gets the service tier information.
*
* @return The service tier information
*/
public String getServiceTier() {
return serviceTier;
}
/**
* Sets the service tier information.
*
* @param serviceTier The service tier information
*/
public void setServiceTier(String serviceTier) {
this.serviceTier = serviceTier;
}
/**
* Gets the cache creation information.
*
* @return The cache creation information
*/
public CacheCreation getCacheCreation() {
return cacheCreation;
}
/**
* Sets the cache creation information.
*
* @param cacheCreation The cache creation information
*/
public void setCacheCreation(CacheCreation cacheCreation) {
this.cacheCreation = cacheCreation;
}
/**
* Represents server tool use information.
*/
public static class ServerToolUse {
/**
* Number of web search requests.
*/
@JSONField(name = "web_search_requests")
private int webSearchRequests;
}
/**
* Represents cache creation information.
*/
public static class CacheCreation {
/**
* Number of ephemeral 1-hour input tokens.
*/
@JSONField(name = "ephemeral_1h_input_tokens")
private int ephemeral1hInputTokens;
/**
* Number of ephemeral 5-minute input tokens.
*/
@JSONField(name = "ephemeral_5m_input_tokens")
private int ephemeral5mInputTokens;
/**
* Gets the number of ephemeral 1-hour input tokens.
*
* @return The number of ephemeral 1-hour input tokens
*/
public int getEphemeral1hInputTokens() {
return ephemeral1hInputTokens;
}
/**
* Sets the number of ephemeral 1-hour input tokens.
*
* @param ephemeral1hInputTokens The number of ephemeral 1-hour input tokens
*/
public void setEphemeral1hInputTokens(int ephemeral1hInputTokens) {
this.ephemeral1hInputTokens = ephemeral1hInputTokens;
}
/**
* Gets the number of ephemeral 5-minute input tokens.
*
* @return The number of ephemeral 5-minute input tokens
*/
public int getEphemeral5mInputTokens() {
return ephemeral5mInputTokens;
}
/**
* Sets the number of ephemeral 5-minute input tokens.
*
* @param ephemeral5mInputTokens The number of ephemeral 5-minute input tokens
*/
public void setEphemeral5mInputTokens(int ephemeral5mInputTokens) {
this.ephemeral5mInputTokens = ephemeral5mInputTokens;
}
}
}

View File

@@ -1,98 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Configuration for initializing the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class InitializeConfig {
/**
* Hooks configuration.
*/
String hooks;
/**
* SDK MCP servers configuration.
*/
String sdkMcpServers;
/**
* MCP servers configuration.
*/
String mcpServers;
/**
* Agents configuration.
*/
String agents;
/**
* Gets the hooks configuration.
*
* @return The hooks configuration
*/
public String getHooks() {
return hooks;
}
/**
* Sets the hooks configuration.
*
* @param hooks The hooks configuration
*/
public void setHooks(String hooks) {
this.hooks = hooks;
}
/**
* Gets the SDK MCP servers configuration.
*
* @return The SDK MCP servers configuration
*/
public String getSdkMcpServers() {
return sdkMcpServers;
}
/**
* Sets the SDK MCP servers configuration.
*
* @param sdkMcpServers The SDK MCP servers configuration
*/
public void setSdkMcpServers(String sdkMcpServers) {
this.sdkMcpServers = sdkMcpServers;
}
/**
* Gets the MCP servers configuration.
*
* @return The MCP servers configuration
*/
public String getMcpServers() {
return mcpServers;
}
/**
* Sets the MCP servers configuration.
*
* @param mcpServers The MCP servers configuration
*/
public void setMcpServers(String mcpServers) {
this.mcpServers = mcpServers;
}
/**
* Gets the agents configuration.
*
* @return The agents configuration
*/
public String getAgents() {
return agents;
}
/**
* Sets the agents configuration.
*
* @param agents The agents configuration
*/
public void setAgents(String agents) {
this.agents = agents;
}
}

View File

@@ -1,142 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Represents usage information for a specific model.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class ModelUsage {
/**
* Number of input tokens.
*/
private int inputTokens;
/**
* Number of output tokens.
*/
private int outputTokens;
/**
* Number of cache read input tokens.
*/
private int cacheReadInputTokens;
/**
* Number of cache creation input tokens.
*/
private int cacheCreationInputTokens;
/**
* Number of web search requests.
*/
private int webSearchRequests;
/**
* Context window size.
*/
private int contextWindow;
/**
* Gets the number of input tokens.
*
* @return The number of input tokens
*/
public int getInputTokens() {
return inputTokens;
}
/**
* Sets the number of input tokens.
*
* @param inputTokens The number of input tokens
*/
public void setInputTokens(int inputTokens) {
this.inputTokens = inputTokens;
}
/**
* Gets the number of output tokens.
*
* @return The number of output tokens
*/
public int getOutputTokens() {
return outputTokens;
}
/**
* Sets the number of output tokens.
*
* @param outputTokens The number of output tokens
*/
public void setOutputTokens(int outputTokens) {
this.outputTokens = outputTokens;
}
/**
* Gets the number of cache read input tokens.
*
* @return The number of cache read input tokens
*/
public int getCacheReadInputTokens() {
return cacheReadInputTokens;
}
/**
* Sets the number of cache read input tokens.
*
* @param cacheReadInputTokens The number of cache read input tokens
*/
public void setCacheReadInputTokens(int cacheReadInputTokens) {
this.cacheReadInputTokens = cacheReadInputTokens;
}
/**
* Gets the number of cache creation input tokens.
*
* @return The number of cache creation input tokens
*/
public int getCacheCreationInputTokens() {
return cacheCreationInputTokens;
}
/**
* Sets the number of cache creation input tokens.
*
* @param cacheCreationInputTokens The number of cache creation input tokens
*/
public void setCacheCreationInputTokens(int cacheCreationInputTokens) {
this.cacheCreationInputTokens = cacheCreationInputTokens;
}
/**
* Gets the number of web search requests.
*
* @return The number of web search requests
*/
public int getWebSearchRequests() {
return webSearchRequests;
}
/**
* Sets the number of web search requests.
*
* @param webSearchRequests The number of web search requests
*/
public void setWebSearchRequests(int webSearchRequests) {
this.webSearchRequests = webSearchRequests;
}
/**
* Gets the context window size.
*
* @return The context window size
*/
public int getContextWindow() {
return contextWindow;
}
/**
* Sets the context window size.
*
* @param contextWindow The context window size
*/
public void setContextWindow(int contextWindow) {
this.contextWindow = contextWindow;
}
}

View File

@@ -1,56 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
/**
* Represents different permission modes for the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public enum PermissionMode {
/**
* Default permission mode.
*/
DEFAULT("default"),
/**
* Plan permission mode.
*/
PLAN("plan"),
/**
* Auto-edit permission mode.
*/
AUTO_EDIT("auto-edit"),
/**
* YOLO permission mode.
*/
YOLO("yolo");
private final String value;
PermissionMode(String value) {
this.value = value;
}
/**
* Gets the string value of the permission mode.
*
* @return The string value of the permission mode
*/
public String getValue() {
return value;
}
/**
* Gets the permission mode from its string value.
*
* @param value The string value
* @return The corresponding permission mode
*/
public static PermissionMode fromValue(String value) {
for (PermissionMode mode : PermissionMode.values()) {
if (mode.value.equals(value)) {
return mode;
}
}
throw new IllegalArgumentException("Unknown permission mode: " + value);
}
}

View File

@@ -1,137 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents usage information for a message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Usage {
/**
* Number of input tokens.
*/
@JSONField(name = "input_tokens")
private Integer inputTokens;
/**
* Number of output tokens.
*/
@JSONField(name = "output_tokens")
private Integer outputTokens;
/**
* Number of cache creation input tokens.
*/
@JSONField(name = "cache_creation_input_tokens")
private Integer cacheCreationInputTokens;
/**
* Number of cache read input tokens.
*/
@JSONField(name = "cache_read_input_tokens")
private Integer cacheReadInputTokens;
/**
* Total number of tokens.
*/
@JSONField(name = "total_tokens")
private Integer totalTokens;
/**
* Gets the number of input tokens.
*
* @return The number of input tokens
*/
public Integer getInputTokens() {
return inputTokens;
}
/**
* Sets the number of input tokens.
*
* @param inputTokens The number of input tokens
*/
public void setInputTokens(Integer inputTokens) {
this.inputTokens = inputTokens;
}
/**
* Gets the number of output tokens.
*
* @return The number of output tokens
*/
public Integer getOutputTokens() {
return outputTokens;
}
/**
* Sets the number of output tokens.
*
* @param outputTokens The number of output tokens
*/
public void setOutputTokens(Integer outputTokens) {
this.outputTokens = outputTokens;
}
/**
* Gets the number of cache creation input tokens.
*
* @return The number of cache creation input tokens
*/
public Integer getCacheCreationInputTokens() {
return cacheCreationInputTokens;
}
/**
* Sets the number of cache creation input tokens.
*
* @param cacheCreationInputTokens The number of cache creation input tokens
*/
public void setCacheCreationInputTokens(Integer cacheCreationInputTokens) {
this.cacheCreationInputTokens = cacheCreationInputTokens;
}
/**
* Gets the number of cache read input tokens.
*
* @return The number of cache read input tokens
*/
public Integer getCacheReadInputTokens() {
return cacheReadInputTokens;
}
/**
* Sets the number of cache read input tokens.
*
* @param cacheReadInputTokens The number of cache read input tokens
*/
public void setCacheReadInputTokens(Integer cacheReadInputTokens) {
this.cacheReadInputTokens = cacheReadInputTokens;
}
/**
* Gets the total number of tokens.
*
* @return The total number of tokens
*/
public Integer getTotalTokens() {
return totalTokens;
}
/**
* Sets the total number of tokens.
*
* @param totalTokens The total number of tokens
*/
public void setTotalTokens(Integer totalTokens) {
this.totalTokens = totalTokens;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -1,46 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents an allow behavior that permits an operation.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "allow")
public class Allow extends Behavior {
/**
* Creates a new Allow instance and sets the behavior to allow.
*/
public Allow() {
super();
this.behavior = Operation.allow;
}
/**
* Updated input for the operation.
*/
Map<String, Object> updatedInput;
/**
* Gets the updated input.
*
* @return The updated input
*/
public Map<String, Object> getUpdatedInput() {
return updatedInput;
}
/**
* Sets the updated input.
*
* @param updatedInput The updated input
* @return This instance for method chaining
*/
public Allow setUpdatedInput(Map<String, Object> updatedInput) {
this.updatedInput = updatedInput;
return this;
}
}

View File

@@ -1,62 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for behavior objects that define how the CLI should handle requests.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "Behavior", seeAlso = {Allow.class, Deny.class})
public class Behavior {
/**
* The behavior operation (allow or deny).
*/
Operation behavior;
/**
* Gets the behavior operation.
*
* @return The behavior operation
*/
public Operation getBehavior() {
return behavior;
}
/**
* Sets the behavior operation.
*
* @param behavior The behavior operation
*/
public void setBehavior(Operation behavior) {
this.behavior = behavior;
}
/**
* Represents the type of operation.
*/
public enum Operation {
/**
* Allow the operation.
*/
allow,
/**
* Deny the operation.
*/
deny
}
/**
* Gets the default behavior (deny with message).
*
* @return The default behavior
*/
public static Behavior defaultBehavior() {
return denyBehavior();
}
public static Behavior denyBehavior() {
return new Deny().setMessage("Default Behavior Permission denied");
}
}

View File

@@ -1,45 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.data.behavior;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a deny behavior that rejects an operation.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "operation", typeName = "deny")
public class Deny extends Behavior {
/**
* Creates a new Deny instance and sets the behavior to deny.
*/
public Deny() {
super();
this.behavior = Operation.deny;
}
/**
* The message explaining why the operation was denied.
*/
String message;
/**
* Gets the denial message.
*
* @return The denial message
*/
public String getMessage() {
return message;
}
/**
* Sets the denial message.
*
* @param message The denial message
* @return This instance for method chaining
*/
public Deny setMessage(String message) {
this.message = message;
return this;
}
}

View File

@@ -1,23 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message;
/**
* Represents a message in the Qwen Code protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public interface Message {
/**
* Gets the type of the message.
*
* @return The type of the message
*/
String getType();
/**
* Gets the ID of the message.
*
* @return The ID of the message
*/
String getMessageId();
}

View File

@@ -1,64 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for messages in the Qwen Code protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(alphabetic = false, typeKey = "type", typeName = "MessageBase")
public class MessageBase implements Message{
/**
* The type of the message.
*/
protected String type;
/**
* The ID of the message.
*/
@JSONField(name = "message_id")
protected String messageId;
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
/** {@inheritDoc} */
@Override
public String getType() {
return type;
}
/**
* Sets the type of the message.
*
* @param type The type of the message
*/
public void setType(String type) {
this.type = type;
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the ID of the message.
*
* @param messageId The ID of the message
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
}

View File

@@ -1,332 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.CLIPermissionDenial;
import com.alibaba.qwen.code.cli.protocol.data.ExtendedUsage;
import com.alibaba.qwen.code.cli.protocol.data.Usage;
/**
* Represents a result message from the SDK.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "result")
public class SDKResultMessage extends MessageBase {
/**
* The subtype of the result.
*/
private String subtype; // 'error_max_turns' | 'error_during_execution'
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* Whether the result represents an error.
*/
@JSONField(name = "is_error")
private boolean isError = true;
/**
* Duration in milliseconds.
*/
@JSONField(name = "duration_ms")
private Long durationMs;
/**
* API duration in milliseconds.
*/
@JSONField(name = "duration_api_ms")
private Long durationApiMs;
/**
* Number of turns.
*/
@JSONField(name = "num_turns")
private Integer numTurns;
/**
* Usage information.
*/
private ExtendedUsage usage;
/**
* Model usage information.
*/
private Map<String, Usage> modelUsage;
/**
* List of permission denials.
*/
@JSONField(name = "permission_denials")
private List<CLIPermissionDenial> permissionDenials;
/**
* Error information.
*/
private Error error;
/**
* Creates a new SDKResultMessage instance and sets the type to "result".
*/
public SDKResultMessage() {
super();
this.type = "result";
}
/**
* Gets the subtype of the result.
*
* @return The subtype of the result
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the result.
*
* @param subtype The subtype of the result
*/
public void setSubtype(String subtype) {
this.subtype = subtype;
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Checks if the result represents an error.
*
* @return Whether the result represents an error
*/
public boolean isError() {
return isError;
}
/**
* Sets whether the result represents an error.
*
* @param error Whether the result represents an error
*/
public void setError(boolean error) {
isError = error;
}
/**
* Gets the duration in milliseconds.
*
* @return The duration in milliseconds
*/
public Long getDurationMs() {
return durationMs;
}
/**
* Sets the duration in milliseconds.
*
* @param durationMs The duration in milliseconds
*/
public void setDurationMs(Long durationMs) {
this.durationMs = durationMs;
}
/**
* Gets the API duration in milliseconds.
*
* @return The API duration in milliseconds
*/
public Long getDurationApiMs() {
return durationApiMs;
}
/**
* Sets the API duration in milliseconds.
*
* @param durationApiMs The API duration in milliseconds
*/
public void setDurationApiMs(Long durationApiMs) {
this.durationApiMs = durationApiMs;
}
/**
* Gets the number of turns.
*
* @return The number of turns
*/
public Integer getNumTurns() {
return numTurns;
}
/**
* Sets the number of turns.
*
* @param numTurns The number of turns
*/
public void setNumTurns(Integer numTurns) {
this.numTurns = numTurns;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public ExtendedUsage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(ExtendedUsage usage) {
this.usage = usage;
}
/**
* Gets the model usage information.
*
* @return The model usage information
*/
public Map<String, Usage> getModelUsage() {
return modelUsage;
}
/**
* Sets the model usage information.
*
* @param modelUsage The model usage information
*/
public void setModelUsage(Map<String, Usage> modelUsage) {
this.modelUsage = modelUsage;
}
/**
* Gets the list of permission denials.
*
* @return The list of permission denials
*/
public List<CLIPermissionDenial> getPermissionDenials() {
return permissionDenials;
}
/**
* Sets the list of permission denials.
*
* @param permissionDenials The list of permission denials
*/
public void setPermissionDenials(List<CLIPermissionDenial> permissionDenials) {
this.permissionDenials = permissionDenials;
}
/**
* Gets the error information.
*
* @return The error information
*/
public Error getError() {
return error;
}
/**
* Sets the error information.
*
* @param error The error information
*/
public void setError(Error error) {
this.error = error;
}
/**
* Represents error information.
*/
public static class Error {
/**
* Error type.
*/
private String type;
/**
* Error message.
*/
private String message;
/**
* Gets the error type.
*
* @return The error type
*/
public String getType() {
return type;
}
/**
* Sets the error type.
*
* @param type The error type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the error message.
*
* @return The error message
*/
public String getMessage() {
return message;
}
/**
* Sets the error message.
*
* @param message The error message
*/
public void setMessage(String message) {
this.message = message;
}
}
}

View File

@@ -1,486 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a system message from the SDK.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "system")
public class SDKSystemMessage extends MessageBase {
/**
* The subtype of the system message.
*/
private String subtype;
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* Additional data.
*/
private Object data;
/**
* Current working directory.
*/
private String cwd;
/**
* List of available tools.
*/
private List<String> tools;
/**
* List of MCP servers.
*/
@JSONField(name = "mcp_servers")
private List<McpServer> mcpServers;
/**
* Model information.
*/
private String model;
/**
* Permission mode.
*/
@JSONField(name = "permission_mode")
private String permissionMode;
/**
* Available slash commands.
*/
@JSONField(name = "slash_commands")
private List<String> slashCommands;
/**
* Qwen Code version.
*/
@JSONField(name = "qwen_code_version")
private String qwenCodeVersion;
/**
* Output style.
*/
@JSONField(name = "output_style")
private String outputStyle;
/**
* Available agents.
*/
private List<String> agents;
/**
* Available skills.
*/
private List<String> skills;
/**
* Capabilities information.
*/
private Map<String, Object> capabilities;
/**
* Compact metadata.
*/
@JSONField(name = "compact_metadata")
private CompactMetadata compactMetadata;
/**
* Creates a new SDKSystemMessage instance and sets the type to "system".
*/
public SDKSystemMessage() {
super();
this.type = "system";
}
/**
* Gets the subtype of the system message.
*
* @return The subtype of the system message
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the system message.
*
* @param subtype The subtype of the system message
*/
public void setSubtype(String subtype) {
this.subtype = subtype;
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the additional data.
*
* @return The additional data
*/
public Object getData() {
return data;
}
/**
* Sets the additional data.
*
* @param data The additional data
*/
public void setData(Object data) {
this.data = data;
}
/**
* Gets the current working directory.
*
* @return The current working directory
*/
public String getCwd() {
return cwd;
}
/**
* Sets the current working directory.
*
* @param cwd The current working directory
*/
public void setCwd(String cwd) {
this.cwd = cwd;
}
/**
* Gets the list of available tools.
*
* @return The list of available tools
*/
public List<String> getTools() {
return tools;
}
/**
* Sets the list of available tools.
*
* @param tools The list of available tools
*/
public void setTools(List<String> tools) {
this.tools = tools;
}
/**
* Gets the list of MCP servers.
*
* @return The list of MCP servers
*/
public List<McpServer> getMcpServers() {
return mcpServers;
}
/**
* Sets the list of MCP servers.
*
* @param mcpServers The list of MCP servers
*/
public void setMcpServers(List<McpServer> mcpServers) {
this.mcpServers = mcpServers;
}
/**
* Gets the model information.
*
* @return The model information
*/
public String getModel() {
return model;
}
/**
* Sets the model information.
*
* @param model The model information
*/
public void setModel(String model) {
this.model = model;
}
/**
* Gets the permission mode.
*
* @return The permission mode
*/
public String getPermissionMode() {
return permissionMode;
}
/**
* Sets the permission mode.
*
* @param permissionMode The permission mode
*/
public void setPermissionMode(String permissionMode) {
this.permissionMode = permissionMode;
}
/**
* Gets the available slash commands.
*
* @return The available slash commands
*/
public List<String> getSlashCommands() {
return slashCommands;
}
/**
* Sets the available slash commands.
*
* @param slashCommands The available slash commands
*/
public void setSlashCommands(List<String> slashCommands) {
this.slashCommands = slashCommands;
}
/**
* Gets the Qwen Code version.
*
* @return The Qwen Code version
*/
public String getQwenCodeVersion() {
return qwenCodeVersion;
}
/**
* Sets the Qwen Code version.
*
* @param qwenCodeVersion The Qwen Code version
*/
public void setQwenCodeVersion(String qwenCodeVersion) {
this.qwenCodeVersion = qwenCodeVersion;
}
/**
* Gets the output style.
*
* @return The output style
*/
public String getOutputStyle() {
return outputStyle;
}
/**
* Sets the output style.
*
* @param outputStyle The output style
*/
public void setOutputStyle(String outputStyle) {
this.outputStyle = outputStyle;
}
/**
* Gets the available agents.
*
* @return The available agents
*/
public List<String> getAgents() {
return agents;
}
/**
* Sets the available agents.
*
* @param agents The available agents
*/
public void setAgents(List<String> agents) {
this.agents = agents;
}
/**
* Gets the available skills.
*
* @return The available skills
*/
public List<String> getSkills() {
return skills;
}
/**
* Sets the available skills.
*
* @param skills The available skills
*/
public void setSkills(List<String> skills) {
this.skills = skills;
}
/**
* Gets the capabilities information.
*
* @return The capabilities information
*/
public Map<String, Object> getCapabilities() {
return capabilities;
}
/**
* Sets the capabilities information.
*
* @param capabilities The capabilities information
*/
public void setCapabilities(Map<String, Object> capabilities) {
this.capabilities = capabilities;
}
/**
* Gets the compact metadata.
*
* @return The compact metadata
*/
public CompactMetadata getCompactMetadata() {
return compactMetadata;
}
/**
* Sets the compact metadata.
*
* @param compactMetadata The compact metadata
*/
public void setCompactMetadata(CompactMetadata compactMetadata) {
this.compactMetadata = compactMetadata;
}
/**
* Represents MCP server information.
*/
public static class McpServer {
/**
* Server name.
*/
private String name;
/**
* Server status.
*/
private String status;
/**
* Gets the server name.
*
* @return The server name
*/
public String getName() {
return name;
}
/**
* Sets the server name.
*
* @param name The server name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the server status.
*
* @return The server status
*/
public String getStatus() {
return status;
}
/**
* Sets the server status.
*
* @param status The server status
*/
public void setStatus(String status) {
this.status = status;
}
}
/**
* Represents compact metadata.
*/
public static class CompactMetadata {
/**
* Trigger information.
*/
private String trigger;
/**
* Pre-tokens information.
*/
@JSONField(name = "pre_tokens")
private Integer preTokens;
/**
* Gets the trigger information.
*
* @return The trigger information
*/
public String getTrigger() {
return trigger;
}
/**
* Sets the trigger information.
*
* @param trigger The trigger information
*/
public void setTrigger(String trigger) {
this.trigger = trigger;
}
/**
* Gets the pre-tokens information.
*
* @return The pre-tokens information
*/
public Integer getPreTokens() {
return preTokens;
}
/**
* Sets the pre-tokens information.
*
* @param preTokens The pre-tokens information
*/
public void setPreTokens(Integer preTokens) {
this.preTokens = preTokens;
}
}
}

View File

@@ -1,196 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a user message in the SDK protocol.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "user")
public class SDKUserMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The API user message.
*/
private final APIUserMessage message = new APIUserMessage();
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Additional options.
*/
private Map<String, String> options;
/**
* Creates a new SDKUserMessage instance and sets the type to "user".
*/
public SDKUserMessage() {
super();
this.setType("user");
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
* @return This instance for method chaining
*/
public SDKUserMessage setSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
/**
* Sets the content of the message.
*
* @param content The content of the message
* @return This instance for method chaining
*/
public SDKUserMessage setContent(String content) {
message.setContent(content);
return this;
}
/**
* Gets the content of the message.
*
* @return The content of the message
*/
public String getContent() {
return message.getContent();
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
* @return This instance for method chaining
*/
public SDKUserMessage setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
return this;
}
/**
* Gets the additional options.
*
* @return The additional options
*/
public Map<String, String> getOptions() {
return options;
}
/**
* Sets the additional options.
*
* @param options The additional options
* @return This instance for method chaining
*/
public SDKUserMessage setOptions(Map<String, String> options) {
this.options = options;
return this;
}
/**
* Represents the API user message.
*/
public static class APIUserMessage {
/**
* User role.
*/
private String role = "user";
/**
* Message content.
*/
private String content;
/**
* Gets the user role.
*
* @return The user role
*/
public String getRole() {
return role;
}
/**
* Sets the user role.
*
* @param role The user role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message content.
*
* @return The message content
*/
public String getContent() {
return content;
}
/**
* Sets the message content.
*
* @param content The message content
*/
public void setContent(String content) {
this.content = content;
}
}
}

View File

@@ -1,172 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import java.util.List;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.qwen.code.cli.protocol.data.Usage;
import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock;
/**
* Represents an API assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class APIAssistantMessage {
/**
* Message ID.
*/
private String id;
/**
* Message type.
*/
private String type = "message";
/**
* Message role.
*/
private String role = "assistant";
/**
* Message model.
*/
private String model;
/**
* Message content.
*/
private List<ContentBlock<?>> content;
/**
* Stop reason.
*/
@JSONField(name = "stop_reason")
private String stopReason;
/**
* Usage information.
*/
private Usage usage;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getId() {
return id;
}
/**
* Sets the message ID.
*
* @param id The message ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the message type.
*
* @return The message type
*/
public String getType() {
return type;
}
/**
* Sets the message type.
*
* @param type The message type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the message role.
*
* @return The message role
*/
public String getRole() {
return role;
}
/**
* Sets the message role.
*
* @param role The message role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message model.
*
* @return The message model
*/
public String getModel() {
return model;
}
/**
* Sets the message model.
*
* @param model The message model
*/
public void setModel(String model) {
this.model = model;
}
/**
* Gets the stop reason.
*
* @return The stop reason
*/
public String getStopReason() {
return stopReason;
}
/**
* Sets the stop reason.
*
* @param stopReason The stop reason
*/
public void setStopReason(String stopReason) {
this.stopReason = stopReason;
}
/**
* Gets the usage information.
*
* @return The usage information
*/
public Usage getUsage() {
return usage;
}
/**
* Sets the usage information.
*
* @param usage The usage information
*/
public void setUsage(Usage usage) {
this.usage = usage;
}
/**
* Gets the message content.
*
* @return The message content
*/
public List<ContentBlock<?>> getContent() {
return content;
}
/**
* Sets the message content.
*
* @param content The message content
*/
public void setContent(List<ContentBlock<?>> content) {
this.content = content;
}
}

View File

@@ -1,121 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
/**
* Represents an SDK assistant message.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "assistant")
public class SDKAssistantMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The API assistant message.
*/
private APIAssistantMessage message;
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Creates a new SDKAssistantMessage instance and sets the type to "assistant".
*/
public SDKAssistantMessage() {
super();
this.type = "assistant";
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return this.getUuid();
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the API assistant message.
*
* @return The API assistant message
*/
public APIAssistantMessage getMessage() {
return message;
}
/**
* Sets the API assistant message.
*
* @param message The API assistant message
*/
public void setMessage(APIAssistantMessage message) {
this.message = message;
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
*/
public void setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
}
}

View File

@@ -1,116 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
import com.alibaba.qwen.code.cli.protocol.message.assistant.event.StreamEvent;
/**
* Represents a partial assistant message during streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "stream_event")
public class SDKPartialAssistantMessage extends MessageBase {
/**
* The UUID of the message.
*/
private String uuid;
/**
* The session ID.
*/
@JSONField(name = "session_id")
private String sessionId;
/**
* The stream event.
*/
private StreamEvent event;
/**
* The parent tool use ID.
*/
@JSONField(name = "parent_tool_use_id")
private String parentToolUseId;
/**
* Creates a new SDKPartialAssistantMessage instance and sets the type to "stream_event".
*/
public SDKPartialAssistantMessage() {
super();
this.type = "stream_event";
}
/**
* Gets the UUID of the message.
*
* @return The UUID of the message
*/
public String getUuid() {
return uuid;
}
/**
* Sets the UUID of the message.
*
* @param uuid The UUID of the message
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Gets the session ID.
*
* @return The session ID
*/
public String getSessionId() {
return sessionId;
}
/**
* Sets the session ID.
*
* @param sessionId The session ID
*/
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
/**
* Gets the stream event.
*
* @return The stream event
*/
public StreamEvent getEvent() {
return event;
}
/**
* Sets the stream event.
*
* @param event The stream event
*/
public void setEvent(StreamEvent event) {
this.event = event;
}
/**
* Gets the parent tool use ID.
*
* @return The parent tool use ID
*/
public String getParentToolUseId() {
return parentToolUseId;
}
/**
* Sets the parent tool use ID.
*
* @param parentToolUseId The parent tool use ID
*/
public void setParentToolUseId(String parentToolUseId) {
this.parentToolUseId = parentToolUseId;
}
}

View File

@@ -1,59 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* Represents an annotation for a content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
public class Annotation {
/**
* The annotation type.
*/
@JSONField(name = "type")
private String type;
/**
* The annotation value.
*/
@JSONField(name = "value")
private String value;
/**
* Gets the annotation type.
*
* @return The annotation type
*/
public String getType() {
return type;
}
/**
* Sets the annotation type.
*
* @param type The annotation type
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the annotation value.
*
* @return The annotation value
*/
public String getValue() {
return value;
}
/**
* Sets the annotation value.
*
* @param value The annotation value
*/
public void setValue(String value) {
this.value = value;
}
}

View File

@@ -1,87 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import java.util.List;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
/**
* Abstract base class for content blocks in assistant messages.
*
* @param <C> The type of content
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "ContentBlock", seeAlso = { TextBlock.class, ToolResultBlock.class, ThinkingBlock.class, ToolUseBlock.class })
public abstract class ContentBlock<C> implements AssistantContent<C> {
/**
* The type of the content block.
*/
protected String type;
/**
* List of annotations.
*/
protected List<Annotation> annotations;
/**
* The message ID.
*/
protected String messageId;
/** {@inheritDoc} */
@Override
public String getType() {
return type;
}
/**
* Sets the type of the content block.
*
* @param type The type of the content block
*/
public void setType(String type) {
this.type = type;
}
/**
* Gets the list of annotations.
*
* @return The list of annotations
*/
public List<Annotation> getAnnotations() {
return annotations;
}
/**
* Sets the list of annotations.
*
* @param annotations The list of annotations
*/
public void setAnnotations(List<Annotation> annotations) {
this.annotations = annotations;
}
/** {@inheritDoc} */
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
/**
* <p>toString.</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@@ -1,42 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
/**
* Represents a text content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "text")
public class TextBlock extends ContentBlock<String> implements TextAssistantContent {
/**
* The text content.
*/
private String text;
/**
* Gets the text content.
*
* @return The text content
*/
public String getText() {
return text;
}
/**
* Sets the text content.
*
* @param text The text content
*/
public void setText(String text) {
this.text = text;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return text;
}
}

View File

@@ -1,64 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
/**
* Represents a thinking content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "thinking")
public class ThinkingBlock extends ContentBlock<String> implements ThingkingAssistantContent {
/**
* The thinking content.
*/
private String thinking;
/**
* The signature.
*/
private String signature;
/**
* Gets the thinking content.
*
* @return The thinking content
*/
public String getThinking() {
return thinking;
}
/**
* Sets the thinking content.
*
* @param thinking The thinking content
*/
public void setThinking(String thinking) {
this.thinking = thinking;
}
/**
* Gets the signature.
*
* @return The signature
*/
public String getSignature() {
return signature;
}
/**
* Sets the signature.
*
* @param signature The signature
*/
public void setSignature(String signature) {
this.signature = signature;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return thinking;
}
}

View File

@@ -1,92 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolResultAssistantContent;
/**
* Represents a tool result content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "tool_result")
public class ToolResultBlock extends ContentBlock<String> implements ToolResultAssistantContent {
/**
* The tool use ID.
*/
@JSONField(name = "tool_use_id")
private String toolUseId;
/**
* The result content.
*/
@JSONField(name = "content")
private String content;
/**
* Whether the result is an error.
*/
@JSONField(name = "is_error")
private Boolean isError;
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
public String getToolUseId() {
return toolUseId;
}
/**
* Sets the tool use ID.
*
* @param toolUseId The tool use ID
*/
public void setToolUseId(String toolUseId) {
this.toolUseId = toolUseId;
}
/**
* Gets the result content.
*
* @return The result content
*/
public String getContent() {
return content;
}
/**
* Sets the result content.
*
* @param content The result content
*/
public void setContent(String content) {
this.content = content;
}
/**
* Gets whether the result is an error.
*
* @return Whether the result is an error
*/
public Boolean getIsError() {
return isError;
}
/**
* Sets whether the result is an error.
*
* @param isError Whether the result is an error
*/
public void setIsError(Boolean isError) {
this.isError = isError;
}
/** {@inheritDoc} */
@Override
public String getContentOfAssistant() {
return content;
}
}

View File

@@ -1,122 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.block;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
/**
* Represents a tool use content block.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "tool_use")
public class ToolUseBlock extends ContentBlock<Map<String, Object>> implements ToolUseAssistantContent {
/**
* The tool use ID.
*/
private String id;
/**
* The tool name.
*/
private String name;
/**
* The tool input.
*/
private Map<String, Object> input;
/**
* List of annotations.
*/
private List<Annotation> annotations;
/**
* Creates a new ToolUseBlock instance.
*/
public ToolUseBlock() {}
/**
* Gets the tool use ID.
*
* @return The tool use ID
*/
public String getId() {
return id;
}
/**
* Sets the tool use ID.
*
* @param id The tool use ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the tool name.
*
* @return The tool name
*/
public String getName() {
return name;
}
/**
* Sets the tool name.
*
* @param name The tool name
*/
public void setName(String name) {
this.name = name;
}
/**
* Gets the tool input.
*
* @return The tool input
*/
public Map<String, Object> getInput() {
return input;
}
/**
* Sets the tool input.
*
* @param input The tool input
*/
public void setInput(Map<String, Object> input) {
this.input = input;
}
/**
* Gets the list of annotations.
*
* @return The list of annotations
*/
public List<Annotation> getAnnotations() {
return annotations;
}
/**
* {@inheritDoc}
*
* Sets the list of annotations.
*/
@Override
public void setAnnotations(List<Annotation> annotations) {
this.annotations = annotations;
}
/**
* {@inheritDoc}
*
* Gets the content of the assistant.
*/
@Override
public Map<String, Object> getContentOfAssistant() {
return Collections.emptyMap();
}
}

View File

@@ -1,224 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import java.util.Map;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.TextAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ThingkingAssistantContent;
import com.alibaba.qwen.code.cli.protocol.data.AssistantContent.ToolUseAssistantContent;
/**
* Represents a content block delta event during streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_delta")
public class ContentBlockDeltaEvent extends StreamEvent {
/**
* The index of the content block.
*/
private int index;
/**
* The content block delta.
*/
private ContentBlockDelta<?> delta;
/**
* Gets the index of the content block.
*
* @return The index of the content block
*/
public int getIndex() {
return index;
}
/**
* Sets the index of the content block.
*
* @param index The index of the content block
*/
public void setIndex(int index) {
this.index = index;
}
/**
* Gets the content block delta.
*
* @return The content block delta
*/
public ContentBlockDelta<?> getDelta() {
return delta;
}
/**
* Sets the content block delta.
*
* @param delta The content block delta
*/
public void setDelta(ContentBlockDelta<?> delta) {
this.delta = delta;
}
/**
* Abstract base class for content block deltas.
*
* @param <C> The type of content
*/
@JSONType(typeKey = "type", typeName = "ContentBlockDelta",
seeAlso = {ContentBlockDeltaText.class, ContentBlockDeltaThinking.class, ContentBlockDeltaInputJson.class})
public abstract static class ContentBlockDelta<C> implements AssistantContent<C> {
/**
* The type of the content block delta.
*/
protected String type;
/**
* The message ID.
*/
protected String messageId;
@Override
public String getType() {
return type;
}
/**
* Sets the type of the content block delta.
*
* @param type The type of the content block delta
*/
public void setType(String type) {
this.type = type;
}
@Override
public String getMessageId() {
return messageId;
}
/**
* Sets the message ID.
*
* @param messageId The message ID
*/
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public String toString() {
return JSON.toJSONString(this);
}
}
/**
* Represents a text delta.
*/
@JSONType(typeKey = "type", typeName = "text_delta")
public static class ContentBlockDeltaText extends ContentBlockDelta<String> implements TextAssistantContent {
/**
* The text content.
*/
private String text;
/**
* Gets the text content.
*
* @return The text content
*/
public String getText() {
return text;
}
/**
* Sets the text content.
*
* @param text The text content
*/
public void setText(String text) {
this.text = text;
}
@Override
public String getContentOfAssistant() {
return text;
}
}
/**
* Represents a thinking delta.
*/
@JSONType(typeKey = "type", typeName = "thinking_delta")
public static class ContentBlockDeltaThinking extends ContentBlockDelta<String> implements ThingkingAssistantContent {
/**
* The thinking content.
*/
private String thinking;
/**
* Gets the thinking content.
*
* @return The thinking content
*/
public String getThinking() {
return thinking;
}
/**
* Sets the thinking content.
*
* @param thinking The thinking content
*/
public void setThinking(String thinking) {
this.thinking = thinking;
}
@Override
public String getContentOfAssistant() {
return thinking;
}
}
/**
* Represents an input JSON delta.
*/
@JSONType(typeKey = "type", typeName = "input_json_delta")
public static class ContentBlockDeltaInputJson extends ContentBlockDelta<Map<String, Object>> implements ToolUseAssistantContent {
/**
* The partial JSON content.
*/
@JSONField(name = "partial_json")
private String partialJson;
/**
* Gets the partial JSON content.
*
* @return The partial JSON content
*/
public String getPartialJson() {
return partialJson;
}
/**
* Sets the partial JSON content.
*
* @param partialJson The partial JSON content
*/
public void setPartialJson(String partialJson) {
this.partialJson = partialJson;
}
@Override
public Map<String, Object> getContentOfAssistant() {
return getInput();
}
@Override
public Map<String, Object> getInput() {
return JSON.parseObject(partialJson, new TypeReference<Map<String, Object>>() {});
}
}
}

View File

@@ -1,25 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.assistant.block.ContentBlock;
/**
* Represents a content block start event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_start")
public class ContentBlockStartEvent extends StreamEvent{
/**
* The index of the content block.
*/
private int index;
/**
* The content block that is starting.
*/
@JSONField(name = "content_block")
private ContentBlock contentBlock;
}

View File

@@ -1,35 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a content block stop event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "content_block_stop")
public class ContentBlockStopEvent extends StreamEvent{
/**
* The index of the content block.
*/
Long index;
/**
* Gets the index of the content block.
*
* @return The index of the content block
*/
public Long getIndex() {
return index;
}
/**
* Sets the index of the content block.
*
* @param index The index of the content block
*/
public void setIndex(Long index) {
this.index = index;
}
}

View File

@@ -1,107 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a message start event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeName = "message_start")
public class MessageStartStreamEvent extends StreamEvent{
/**
* The message that is starting.
*/
private Message message;
/**
* Represents the message information.
*/
public static class Message {
/**
* Message ID.
*/
private String id;
/**
* Message role.
*/
private String role;
/**
* Message model.
*/
private String model;
/**
* Gets the message ID.
*
* @return The message ID
*/
public String getId() {
return id;
}
/**
* Sets the message ID.
*
* @param id The message ID
*/
public void setId(String id) {
this.id = id;
}
/**
* Gets the message role.
*
* @return The message role
*/
public String getRole() {
return role;
}
/**
* Sets the message role.
*
* @param role The message role
*/
public void setRole(String role) {
this.role = role;
}
/**
* Gets the message model.
*
* @return The message model
*/
public String getModel() {
return model;
}
/**
* Sets the message model.
*
* @param model The message model
*/
public void setModel(String model) {
this.model = model;
}
}
/**
* Gets the message that is starting.
*
* @return The message that is starting
*/
public Message getMessage() {
return message;
}
/**
* Sets the message that is starting.
*
* @param message The message that is starting
*/
public void setMessage(Message message) {
this.message = message;
}
}

View File

@@ -1,13 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Represents a message stop event during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeName = "message_stop")
public class MessageStopStreamEvent extends StreamEvent{
}

View File

@@ -1,37 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.assistant.event;
import com.alibaba.fastjson2.annotation.JSONType;
/**
* Base class for stream events during message streaming.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "StreamEvent",
seeAlso = {MessageStartStreamEvent.class, MessageStopStreamEvent.class, ContentBlockStartEvent.class, ContentBlockStopEvent.class,
ContentBlockDeltaEvent.class})
public class StreamEvent {
/**
* The type of the stream event.
*/
protected String type;
/**
* Gets the type of the stream event.
*
* @return The type of the stream event
*/
public String getType() {
return type;
}
/**
* Sets the type of the stream event.
*
* @param type The type of the stream event
*/
public void setType(String type) {
this.type = type;
}
}

View File

@@ -1,90 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.control;
import java.util.UUID;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
import com.alibaba.qwen.code.cli.protocol.message.control.payload.ControlRequestPayload;
/**
* Represents a control request to the CLI.
*
* @param <R> The type of the request object
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "control_request")
public class CLIControlRequest<R extends ControlRequestPayload> extends MessageBase {
/**
* The ID of the request.
*/
@JSONField(name = "request_id")
private String requestId = UUID.randomUUID().toString();
/**
* The actual request object.
*/
private R request;
/**
* Creates a new CLIControlRequest instance and sets the type to "control_request".
*/
public CLIControlRequest() {
super();
type = "control_request";
}
/**
* Creates a new control request with the specified request object.
*
* @param request The request object
* @param <T> The type of the request object
* @return A new control request instance
*/
public static <T extends ControlRequestPayload> CLIControlRequest<T> create(T request) {
CLIControlRequest<T> controlRequest = new CLIControlRequest<>();
controlRequest.setRequest(request);
return controlRequest;
}
/**
* Gets the ID of the request.
*
* @return The ID of the request
*/
public String getRequestId() {
return requestId;
}
/**
* Sets the ID of the request.
*
* @param requestId The ID of the request
* @return This instance for method chaining
*/
public CLIControlRequest<R> setRequestId(String requestId) {
this.requestId = requestId;
return this;
}
/**
* Gets the actual request object.
*
* @return The actual request object
*/
public R getRequest() {
return request;
}
/**
* Sets the actual request object.
*
* @param request The actual request object
* @return This instance for method chaining
*/
public CLIControlRequest<R> setRequest(R request) {
this.request = request;
return this;
}
}

View File

@@ -1,138 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.control;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.message.MessageBase;
/**
* Represents a control response from the CLI.
*
* @param <R> The type of the response object
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "type", typeName = "control_response")
public class CLIControlResponse<R> extends MessageBase {
/**
* The response object.
*/
private Response<R> response;
/**
* Creates a new CLIControlResponse instance and sets the type to "control_response".
*/
public CLIControlResponse() {
super();
this.type = "control_response";
}
/**
* Gets the response object.
*
* @return The response object
*/
public Response<R> getResponse() {
return response;
}
/**
* Sets the response object.
*
* @param response The response object
*/
public void setResponse(Response<R> response) {
this.response = response;
}
/**
* Creates a new response object.
*
* @return A new response object
*/
public Response<R> createResponse() {
Response<R> response = new Response<>();
this.setResponse(response);
return response;
}
/**
* Represents the response information.
*
* @param <R> The type of the response object
*/
public static class Response<R> {
/**
* The ID of the request.
*/
@JSONField(name = "request_id")
private String requestId;
/**
* The subtype of the response.
*/
private String subtype = "success";
/**
* The actual response.
*/
R response;
/**
* Gets the ID of the request.
*
* @return The ID of the request
*/
public String getRequestId() {
return requestId;
}
/**
* Sets the ID of the request.
*
* @param requestId The ID of the request
* @return This instance for method chaining
*/
public Response<R> setRequestId(String requestId) {
this.requestId = requestId;
return this;
}
/**
* Gets the subtype of the response.
*
* @return The subtype of the response
*/
public String getSubtype() {
return subtype;
}
/**
* Sets the subtype of the response.
*
* @param subtype The subtype of the response
* @return This instance for method chaining
*/
public Response<R> setSubtype(String subtype) {
this.subtype = subtype;
return this;
}
/**
* Gets the actual response.
*
* @return The actual response
*/
public R getResponse() {
return response;
}
/**
* Sets the actual response.
*
* @param response The actual response
* @return This instance for method chaining
*/
public Response<R> setResponse(R response) {
this.response = response;
return this;
}
}
}

View File

@@ -1,45 +0,0 @@
package com.alibaba.qwen.code.cli.protocol.message.control.payload;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import com.alibaba.qwen.code.cli.protocol.data.InitializeConfig;
/**
* Represents a control initialize request to the CLI.
*
* @author skyfire
* @version $Id: 0.0.1
*/
@JSONType(typeKey = "subtype", typeName = "initialize")
public class CLIControlInitializeRequest extends ControlRequestPayload {
public CLIControlInitializeRequest() {
super();
this.subtype = "initialize";
}
/**
* The initialization configuration.
*/
@JSONField(unwrapped = true)
InitializeConfig initializeConfig = new InitializeConfig();
/**
* Gets the initialization configuration.
*
* @return The initialization configuration
*/
public InitializeConfig getInitializeConfig() {
return initializeConfig;
}
/**
* Sets the initialization configuration.
*
* @param initializeConfig The initialization configuration
* @return This instance for method chaining
*/
public CLIControlInitializeRequest setInitializeConfig(InitializeConfig initializeConfig) {
this.initializeConfig = initializeConfig;
return this;
}
}

Some files were not shown because too many files have changed in this diff Show More