diff --git a/.gitignore b/.gitignore
index fac00d412..705216c80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ package-lock.json
.idea
*.iml
.cursor
+.qoder
# OS metadata
.DS_Store
diff --git a/docs/developers/_meta.ts b/docs/developers/_meta.ts
index 956e1ad98..154ce1848 100644
--- a/docs/developers/_meta.ts
+++ b/docs/developers/_meta.ts
@@ -11,6 +11,7 @@ export default {
type: 'separator',
},
'sdk-typescript': 'Typescript SDK',
+ 'sdk-java': 'Java SDK(alpha)',
'Dive Into Qwen Code': {
title: 'Dive Into Qwen Code',
type: 'separator',
diff --git a/docs/developers/sdk-java.md b/docs/developers/sdk-java.md
new file mode 100644
index 000000000..0b16e60a5
--- /dev/null
+++ b/docs/developers/sdk-java.md
@@ -0,0 +1,312 @@
+# 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
+
+ com.alibaba
+ qwencode-sdk
+ {$version}
+
+```
+
+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 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() {{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 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, 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.
diff --git a/docs/users/configuration/settings.md b/docs/users/configuration/settings.md
index 3e87985b8..9cf704dae 100644
--- a/docs/users/configuration/settings.md
+++ b/docs/users/configuration/settings.md
@@ -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). | | |
-| `--experimental-acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Experimental. |
+| `--acp` | | Enables ACP mode (Agent Control Protocol). Useful for IDE/editor integrations like [Zed](../integration-zed). | | Stable. Replaces the deprecated `--experimental-acp` flag. |
| `--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. | | |
diff --git a/docs/users/integration-zed.md b/docs/users/integration-zed.md
index cd4cb2ae4..663e23e80 100644
--- a/docs/users/integration-zed.md
+++ b/docs/users/integration-zed.md
@@ -32,7 +32,7 @@
"Qwen Code": {
"type": "custom",
"command": "qwen",
- "args": ["--experimental-acp"],
+ "args": ["--acp"],
"env": {}
}
```
diff --git a/integration-tests/acp-integration.test.ts b/integration-tests/acp-integration.test.ts
index 31e32da76..b89292d87 100644
--- a/integration-tests/acp-integration.test.ts
+++ b/integration-tests/acp-integration.test.ts
@@ -80,10 +80,11 @@ 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 },
+ options?: { permissionHandler?: PermissionHandler; useNewFlag?: boolean },
) {
const pending = new Map();
let nextRequestId = 1;
@@ -95,9 +96,13 @@ 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, '--experimental-acp', '--no-chat-recording'],
+ [rig.bundlePath, acpFlag, '--no-chat-recording'],
{
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -621,3 +626,99 @@ 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();
+ }
+ });
+ },
+);
diff --git a/package-lock.json b/package-lock.json
index 330b90e08..0ed7071f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
- "version": "0.6.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
- "version": "0.6.0",
+ "version": "0.6.1",
"workspaces": [
"packages/*"
],
@@ -17316,7 +17316,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
- "version": "0.6.0",
+ "version": "0.6.1",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17953,7 +17953,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
- "version": "0.6.0",
+ "version": "0.6.1",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21413,7 +21413,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
- "version": "0.6.0",
+ "version": "0.6.1",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21425,7 +21425,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
- "version": "0.6.0",
+ "version": "0.6.1",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
diff --git a/package.json b/package.json
index c239067ff..107b9e9b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
- "version": "0.6.0",
+ "version": "0.6.1",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
- "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
+ "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1"
},
"scripts": {
"start": "cross-env node scripts/start.js",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index f2083fe19..2154e4683 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
- "version": "0.6.0",
+ "version": "0.6.1",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
- "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
+ "sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.1"
},
"dependencies": {
"@google/genai": "1.30.0",
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index 0b95f7857..6f2019e75 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -1597,6 +1597,58 @@ 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',
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 3f781c46e..cfeb3560c 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -10,22 +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,
type ResumedSessionData,
type FileFilteringOptions,
type MCPServerConfig,
+ type ToolName,
+ EditTool,
+ ShellTool,
+ WriteFileTool,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import type { Settings } from './settings.js';
@@ -111,6 +113,7 @@ export interface CliArgs {
telemetryOutfile: string | undefined;
allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined;
+ acp: boolean | undefined;
experimentalAcp: boolean | undefined;
experimentalSkills: boolean | undefined;
extensions: string[] | undefined;
@@ -314,10 +317,16 @@ export async function parseArguments(settings: Settings): Promise {
description: 'Enables checkpointing of file edits',
default: false,
})
- .option('experimental-acp', {
+ .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,
+ })
.option('experimental-skills', {
type: 'boolean',
description: 'Enable experimental Skills feature',
@@ -599,8 +608,19 @@ export async function parseArguments(settings: Settings): Promise {
// The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument
- // Apply ACP fallback: if experimental-acp is present but no explicit --channel, treat as ACP
- if (result['experimentalAcp'] && !result['channel']) {
+ // 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)['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']) {
(result as Record)['channel'] = 'ACP';
}
@@ -828,6 +848,28 @@ 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 &&
@@ -836,12 +878,15 @@ export async function loadCliConfig(
switch (approvalMode) {
case ApprovalMode.PLAN:
case ApprovalMode.DEFAULT:
- // In default non-interactive mode, all tools that require approval are excluded.
- extraExcludes.push(ShellTool.Name, EditTool.Name, WriteFileTool.Name);
+ // 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);
break;
case ApprovalMode.AUTO_EDIT:
// In auto-edit non-interactive mode, only tools that still require a prompt are excluded.
- extraExcludes.push(ShellTool.Name);
+ excludeUnlessExplicit(ShellTool.Name as ToolName);
break;
case ApprovalMode.YOLO:
// No extra excludes for YOLO mode.
@@ -991,7 +1036,7 @@ export async function loadCliConfig(
sessionTokenLimit: settings.model?.sessionTokenLimit ?? -1,
maxSessionTurns:
argv.maxSessionTurns ?? settings.model?.maxSessionTurns ?? -1,
- experimentalZedIntegration: argv.experimentalAcp || false,
+ experimentalZedIntegration: argv.acp || argv.experimentalAcp || false,
experimentalSkills: argv.experimentalSkills || false,
listExtensions: argv.listExtensions || false,
extensions: allExtensions,
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 2fe467ba9..5159613b6 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -202,6 +202,7 @@ const SETTINGS_SCHEMA = {
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文 (Chinese)' },
{ value: 'ru', label: 'Русский (Russian)' },
+ { value: 'de', label: 'Deutsch (German)' },
],
},
terminalBell: {
diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx
index 9fa0b8261..9e0137fb7 100644
--- a/packages/cli/src/gemini.test.tsx
+++ b/packages/cli/src/gemini.test.tsx
@@ -460,6 +460,7 @@ describe('gemini.tsx main function kitty protocol', () => {
telemetryOutfile: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
+ acp: undefined,
experimentalAcp: undefined,
experimentalSkills: undefined,
extensions: undefined,
@@ -639,4 +640,37 @@ 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();
+ });
});
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index b05f12453..da945546d 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -183,16 +183,18 @@ export async function startInteractiveUI(
},
);
- checkForUpdates()
- .then((info) => {
- handleAutoUpdate(info, settings, config.getProjectRoot());
- })
- .catch((err) => {
- // Silently ignore update check errors.
- if (config.getDebugMode()) {
- console.error('Update check failed:', err);
- }
- });
+ 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);
+ }
+ });
+ }
registerCleanup(() => instance.unmount());
}
diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js
new file mode 100644
index 000000000..832dd1333
--- /dev/null
+++ b/packages/cli/src/i18n/locales/de.js
@@ -0,0 +1,1073 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// German translations for Qwen Code CLI
+// Deutsche Übersetzungen für Qwen Code CLI
+
+export default {
+ // ============================================================================
+ // Help / UI Components
+ // ============================================================================
+ 'Basics:': 'Grundlagen:',
+ 'Add context': 'Kontext hinzufügen',
+ 'Use {{symbol}} to specify files for context (e.g., {{example}}) to target specific files or folders.':
+ 'Verwenden Sie {{symbol}}, um Dateien als Kontext anzugeben (z.B. {{example}}), um bestimmte Dateien oder Ordner auszuwählen.',
+ '@': '@',
+ '@src/myFile.ts': '@src/myFile.ts',
+ 'Shell mode': 'Shell-Modus',
+ 'YOLO mode': 'YOLO-Modus',
+ 'plan mode': 'Planungsmodus',
+ 'auto-accept edits': 'Änderungen automatisch akzeptieren',
+ 'Accepting edits': 'Änderungen werden akzeptiert',
+ '(shift + tab to cycle)': '(Umschalt + Tab zum Wechseln)',
+ 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).':
+ 'Shell-Befehle über {{symbol}} ausführen (z.B. {{example1}}) oder natürliche Sprache verwenden (z.B. {{example2}}).',
+ '!': '!',
+ '!npm run start': '!npm run start',
+ 'start server': 'Server starten',
+ 'Commands:': 'Befehle:',
+ 'shell command': 'Shell-Befehl',
+ 'Model Context Protocol command (from external servers)':
+ 'Model Context Protocol Befehl (von externen Servern)',
+ 'Keyboard Shortcuts:': 'Tastenkürzel:',
+ 'Jump through words in the input': 'Wörter in der Eingabe überspringen',
+ 'Close dialogs, cancel requests, or quit application':
+ 'Dialoge schließen, Anfragen abbrechen oder Anwendung beenden',
+ 'New line': 'Neue Zeile',
+ 'New line (Alt+Enter works for certain linux distros)':
+ 'Neue Zeile (Alt+Enter funktioniert bei bestimmten Linux-Distributionen)',
+ 'Clear the screen': 'Bildschirm löschen',
+ 'Open input in external editor': 'Eingabe in externem Editor öffnen',
+ 'Send message': 'Nachricht senden',
+ 'Initializing...': 'Initialisierung...',
+ 'Connecting to MCP servers... ({{connected}}/{{total}})':
+ 'Verbindung zu MCP-Servern wird hergestellt... ({{connected}}/{{total}})',
+ 'Type your message or @path/to/file': 'Nachricht eingeben oder @Pfad/zur/Datei',
+ "Press 'i' for INSERT mode and 'Esc' for NORMAL mode.":
+ "Drücken Sie 'i' für den EINFÜGE-Modus und 'Esc' für den NORMAL-Modus.",
+ 'Cancel operation / Clear input (double press)':
+ 'Vorgang abbrechen / Eingabe löschen (doppelt drücken)',
+ 'Cycle approval modes': 'Genehmigungsmodi durchschalten',
+ 'Cycle through your prompt history': 'Eingabeverlauf durchblättern',
+ 'For a full list of shortcuts, see {{docPath}}':
+ 'Eine vollständige Liste der Tastenkürzel finden Sie unter {{docPath}}',
+ 'docs/keyboard-shortcuts.md': 'docs/keyboard-shortcuts.md',
+ 'for help on Qwen Code': 'für Hilfe zu Qwen Code',
+ 'show version info': 'Versionsinformationen anzeigen',
+ 'submit a bug report': 'Fehlerbericht einreichen',
+ 'About Qwen Code': 'Über Qwen Code',
+
+ // ============================================================================
+ // System Information Fields
+ // ============================================================================
+ 'CLI Version': 'CLI-Version',
+ 'Git Commit': 'Git-Commit',
+ Model: 'Modell',
+ Sandbox: 'Sandbox',
+ 'OS Platform': 'Betriebssystem',
+ 'OS Arch': 'OS-Architektur',
+ 'OS Release': 'OS-Version',
+ 'Node.js Version': 'Node.js-Version',
+ 'NPM Version': 'NPM-Version',
+ 'Session ID': 'Sitzungs-ID',
+ 'Auth Method': 'Authentifizierungsmethode',
+ 'Base URL': 'Basis-URL',
+ 'Memory Usage': 'Speichernutzung',
+ 'IDE Client': 'IDE-Client',
+
+ // ============================================================================
+ // Commands - General
+ // ============================================================================
+ 'Analyzes the project and creates a tailored QWEN.md file.':
+ 'Analysiert das Projekt und erstellt eine maßgeschneiderte QWEN.md-Datei.',
+ 'list available Qwen Code tools. Usage: /tools [desc]':
+ 'Verfügbare Qwen Code Werkzeuge auflisten. Verwendung: /tools [desc]',
+ 'Available Qwen Code CLI tools:': 'Verfügbare Qwen Code CLI-Werkzeuge:',
+ 'No tools available': 'Keine Werkzeuge verfügbar',
+ 'View or change the approval mode for tool usage':
+ 'Genehmigungsmodus für Werkzeugnutzung anzeigen oder ändern',
+ 'View or change the language setting': 'Spracheinstellung anzeigen oder ändern',
+ 'change the theme': 'Design ändern',
+ 'Select Theme': 'Design auswählen',
+ Preview: 'Vorschau',
+ '(Use Enter to select, Tab to configure scope)':
+ '(Enter zum Auswählen, Tab zum Konfigurieren des Bereichs)',
+ '(Use Enter to apply scope, Tab to select theme)':
+ '(Enter zum Anwenden des Bereichs, Tab zum Auswählen des Designs)',
+ 'Theme configuration unavailable due to NO_COLOR env variable.':
+ 'Design-Konfiguration aufgrund der NO_COLOR-Umgebungsvariable nicht verfügbar.',
+ 'Theme "{{themeName}}" not found.': 'Design "{{themeName}}" nicht gefunden.',
+ 'Theme "{{themeName}}" not found in selected scope.':
+ 'Design "{{themeName}}" im ausgewählten Bereich nicht gefunden.',
+ 'Clear conversation history and free up context':
+ 'Gesprächsverlauf löschen und Kontext freigeben',
+ 'Compresses the context by replacing it with a summary.':
+ 'Komprimiert den Kontext durch Ersetzen mit einer Zusammenfassung.',
+ 'open full Qwen Code documentation in your browser':
+ 'Vollständige Qwen Code Dokumentation im Browser öffnen',
+ 'Configuration not available.': 'Konfiguration nicht verfügbar.',
+ 'change the auth method': 'Authentifizierungsmethode ändern',
+ 'Copy the last result or code snippet to clipboard':
+ 'Letztes Ergebnis oder Codeausschnitt in die Zwischenablage kopieren',
+
+ // ============================================================================
+ // Commands - Agents
+ // ============================================================================
+ 'Manage subagents for specialized task delegation.':
+ 'Unteragenten für spezialisierte Aufgabendelegation verwalten.',
+ 'Manage existing subagents (view, edit, delete).':
+ 'Bestehende Unteragenten verwalten (anzeigen, bearbeiten, löschen).',
+ 'Create a new subagent with guided setup.':
+ 'Neuen Unteragenten mit geführter Einrichtung erstellen.',
+
+ // ============================================================================
+ // Agents - Management Dialog
+ // ============================================================================
+ Agents: 'Agenten',
+ 'Choose Action': 'Aktion wählen',
+ 'Edit {{name}}': '{{name}} bearbeiten',
+ 'Edit Tools: {{name}}': 'Werkzeuge bearbeiten: {{name}}',
+ 'Edit Color: {{name}}': 'Farbe bearbeiten: {{name}}',
+ 'Delete {{name}}': '{{name}} löschen',
+ 'Unknown Step': 'Unbekannter Schritt',
+ 'Esc to close': 'Esc zum Schließen',
+ 'Enter to select, ↑↓ to navigate, Esc to close':
+ 'Enter zum Auswählen, ↑↓ zum Navigieren, Esc zum Schließen',
+ 'Esc to go back': 'Esc zum Zurückgehen',
+ 'Enter to confirm, Esc to cancel': 'Enter zum Bestätigen, Esc zum Abbrechen',
+ 'Enter to select, ↑↓ to navigate, Esc to go back':
+ 'Enter zum Auswählen, ↑↓ zum Navigieren, Esc zum Zurückgehen',
+ 'Invalid step: {{step}}': 'Ungültiger Schritt: {{step}}',
+ 'No subagents found.': 'Keine Unteragenten gefunden.',
+ "Use '/agents create' to create your first subagent.":
+ "Verwenden Sie '/agents create', um Ihren ersten Unteragenten zu erstellen.",
+ '(built-in)': '(integriert)',
+ '(overridden by project level agent)': '(überschrieben durch Projektagent)',
+ 'Project Level ({{path}})': 'Projektebene ({{path}})',
+ 'User Level ({{path}})': 'Benutzerebene ({{path}})',
+ 'Built-in Agents': 'Integrierte Agenten',
+ 'Using: {{count}} agents': 'Verwendet: {{count}} Agenten',
+ 'View Agent': 'Agent anzeigen',
+ 'Edit Agent': 'Agent bearbeiten',
+ 'Delete Agent': 'Agent löschen',
+ Back: 'Zurück',
+ 'No agent selected': 'Kein Agent ausgewählt',
+ 'File Path: ': 'Dateipfad: ',
+ 'Tools: ': 'Werkzeuge: ',
+ 'Color: ': 'Farbe: ',
+ 'Description:': 'Beschreibung:',
+ 'System Prompt:': 'System-Prompt:',
+ 'Open in editor': 'Im Editor öffnen',
+ 'Edit tools': 'Werkzeuge bearbeiten',
+ 'Edit color': 'Farbe bearbeiten',
+ '❌ Error:': '❌ Fehler:',
+ 'Are you sure you want to delete agent "{{name}}"?':
+ 'Sind Sie sicher, dass Sie den Agenten "{{name}}" löschen möchten?',
+ // ============================================================================
+ // Agents - Creation Wizard
+ // ============================================================================
+ 'Project Level (.qwen/agents/)': 'Projektebene (.qwen/agents/)',
+ 'User Level (~/.qwen/agents/)': 'Benutzerebene (~/.qwen/agents/)',
+ '✅ Subagent Created Successfully!': '✅ Unteragent erfolgreich erstellt!',
+ 'Subagent "{{name}}" has been saved to {{level}} level.':
+ 'Unteragent "{{name}}" wurde auf {{level}}-Ebene gespeichert.',
+ 'Name: ': 'Name: ',
+ 'Location: ': 'Speicherort: ',
+ '❌ Error saving subagent:': '❌ Fehler beim Speichern des Unteragenten:',
+ 'Warnings:': 'Warnungen:',
+ 'Name "{{name}}" already exists at {{level}} level - will overwrite existing subagent':
+ 'Name "{{name}}" existiert bereits auf {{level}}-Ebene - bestehender Unteragent wird überschrieben',
+ 'Name "{{name}}" exists at user level - project level will take precedence':
+ 'Name "{{name}}" existiert auf Benutzerebene - Projektebene hat Vorrang',
+ 'Name "{{name}}" exists at project level - existing subagent will take precedence':
+ 'Name "{{name}}" existiert auf Projektebene - bestehender Unteragent hat Vorrang',
+ 'Description is over {{length}} characters':
+ 'Beschreibung ist über {{length}} Zeichen',
+ 'System prompt is over {{length}} characters':
+ 'System-Prompt ist über {{length}} Zeichen',
+ // Agents - Creation Wizard Steps
+ 'Step {{n}}: Choose Location': 'Schritt {{n}}: Speicherort wählen',
+ 'Step {{n}}: Choose Generation Method':
+ 'Schritt {{n}}: Generierungsmethode wählen',
+ 'Generate with Qwen Code (Recommended)':
+ 'Mit Qwen Code generieren (Empfohlen)',
+ 'Manual Creation': 'Manuelle Erstellung',
+ 'Describe what this subagent should do and when it should be used. (Be comprehensive for best results)':
+ 'Beschreiben Sie, was dieser Unteragent tun soll und wann er verwendet werden soll. (Ausführliche Beschreibung für beste Ergebnisse)',
+ 'e.g., Expert code reviewer that reviews code based on best practices...':
+ 'z.B. Experte für Code-Reviews, der Code nach Best Practices überprüft...',
+ 'Generating subagent configuration...':
+ 'Unteragent-Konfiguration wird generiert...',
+ 'Failed to generate subagent: {{error}}':
+ 'Fehler beim Generieren des Unteragenten: {{error}}',
+ 'Step {{n}}: Describe Your Subagent': 'Schritt {{n}}: Unteragent beschreiben',
+ 'Step {{n}}: Enter Subagent Name': 'Schritt {{n}}: Unteragent-Name eingeben',
+ 'Step {{n}}: Enter System Prompt': 'Schritt {{n}}: System-Prompt eingeben',
+ 'Step {{n}}: Enter Description': 'Schritt {{n}}: Beschreibung eingeben',
+ // Agents - Tool Selection
+ 'Step {{n}}: Select Tools': 'Schritt {{n}}: Werkzeuge auswählen',
+ 'All Tools (Default)': 'Alle Werkzeuge (Standard)',
+ 'All Tools': 'Alle Werkzeuge',
+ 'Read-only Tools': 'Nur-Lese-Werkzeuge',
+ 'Read & Edit Tools': 'Lese- und Bearbeitungswerkzeuge',
+ 'Read & Edit & Execution Tools': 'Lese-, Bearbeitungs- und Ausführungswerkzeuge',
+ 'All tools selected, including MCP tools':
+ 'Alle Werkzeuge ausgewählt, einschließlich MCP-Werkzeuge',
+ 'Selected tools:': 'Ausgewählte Werkzeuge:',
+ 'Read-only tools:': 'Nur-Lese-Werkzeuge:',
+ 'Edit tools:': 'Bearbeitungswerkzeuge:',
+ 'Execution tools:': 'Ausführungswerkzeuge:',
+ 'Step {{n}}: Choose Background Color': 'Schritt {{n}}: Hintergrundfarbe wählen',
+ 'Step {{n}}: Confirm and Save': 'Schritt {{n}}: Bestätigen und Speichern',
+ // Agents - Navigation & Instructions
+ 'Esc to cancel': 'Esc zum Abbrechen',
+ 'Press Enter to save, e to save and edit, Esc to go back':
+ 'Enter zum Speichern, e zum Speichern und Bearbeiten, Esc zum Zurückgehen',
+ 'Press Enter to continue, {{navigation}}Esc to {{action}}':
+ 'Enter zum Fortfahren, {{navigation}}Esc zum {{action}}',
+ cancel: 'Abbrechen',
+ 'go back': 'Zurückgehen',
+ '↑↓ to navigate, ': '↑↓ zum Navigieren, ',
+ 'Enter a clear, unique name for this subagent.':
+ 'Geben Sie einen eindeutigen Namen für diesen Unteragenten ein.',
+ 'e.g., Code Reviewer': 'z.B. Code-Reviewer',
+ 'Name cannot be empty.': 'Name darf nicht leer sein.',
+ "Write the system prompt that defines this subagent's behavior. Be comprehensive for best results.":
+ 'Schreiben Sie den System-Prompt, der das Verhalten dieses Unteragenten definiert. Ausführlich für beste Ergebnisse.',
+ 'e.g., You are an expert code reviewer...':
+ 'z.B. Sie sind ein Experte für Code-Reviews...',
+ 'System prompt cannot be empty.': 'System-Prompt darf nicht leer sein.',
+ 'Describe when and how this subagent should be used.':
+ 'Beschreiben Sie, wann und wie dieser Unteragent verwendet werden soll.',
+ 'e.g., Reviews code for best practices and potential bugs.':
+ 'z.B. Überprüft Code auf Best Practices und mögliche Fehler.',
+ 'Description cannot be empty.': 'Beschreibung darf nicht leer sein.',
+ 'Failed to launch editor: {{error}}': 'Fehler beim Starten des Editors: {{error}}',
+ 'Failed to save and edit subagent: {{error}}':
+ 'Fehler beim Speichern und Bearbeiten des Unteragenten: {{error}}',
+
+ // ============================================================================
+ // Commands - General (continued)
+ // ============================================================================
+ 'View and edit Qwen Code settings': 'Qwen Code Einstellungen anzeigen und bearbeiten',
+ Settings: 'Einstellungen',
+ '(Use Enter to select{{tabText}})': '(Enter zum Auswählen{{tabText}})',
+ ', Tab to change focus': ', Tab zum Fokuswechsel',
+ 'To see changes, Qwen Code must be restarted. Press r to exit and apply changes now.':
+ 'Um Änderungen zu sehen, muss Qwen Code neu gestartet werden. Drücken Sie r, um jetzt zu beenden und Änderungen anzuwenden.',
+ 'The command "/{{command}}" is not supported in non-interactive mode.':
+ 'Der Befehl "/{{command}}" wird im nicht-interaktiven Modus nicht unterstützt.',
+ // ============================================================================
+ // Settings Labels
+ // ============================================================================
+ 'Vim Mode': 'Vim-Modus',
+ 'Disable Auto Update': 'Automatische Updates deaktivieren',
+ 'Enable Prompt Completion': 'Eingabevervollständigung aktivieren',
+ 'Debug Keystroke Logging': 'Debug-Protokollierung von Tastatureingaben',
+ Language: 'Sprache',
+ 'Output Format': 'Ausgabeformat',
+ 'Hide Window Title': 'Fenstertitel ausblenden',
+ 'Show Status in Title': 'Status im Titel anzeigen',
+ 'Hide Tips': 'Tipps ausblenden',
+ 'Hide Banner': 'Banner ausblenden',
+ 'Hide Context Summary': 'Kontextzusammenfassung ausblenden',
+ 'Hide CWD': 'Arbeitsverzeichnis ausblenden',
+ 'Hide Sandbox Status': 'Sandbox-Status ausblenden',
+ 'Hide Model Info': 'Modellinformationen ausblenden',
+ 'Hide Footer': 'Fußzeile ausblenden',
+ 'Show Memory Usage': 'Speichernutzung anzeigen',
+ 'Show Line Numbers': 'Zeilennummern anzeigen',
+ 'Show Citations': 'Quellenangaben anzeigen',
+ 'Custom Witty Phrases': 'Benutzerdefinierte Witzige Sprüche',
+ 'Enable Welcome Back': 'Willkommen-zurück aktivieren',
+ 'Disable Loading Phrases': 'Ladesprüche deaktivieren',
+ 'Screen Reader Mode': 'Bildschirmleser-Modus',
+ 'IDE Mode': 'IDE-Modus',
+ 'Max Session Turns': 'Maximale Sitzungsrunden',
+ 'Skip Next Speaker Check': 'Nächste-Sprecher-Prüfung überspringen',
+ 'Skip Loop Detection': 'Schleifenerkennung überspringen',
+ 'Skip Startup Context': 'Startkontext überspringen',
+ 'Enable OpenAI Logging': 'OpenAI-Protokollierung aktivieren',
+ 'OpenAI Logging Directory': 'OpenAI-Protokollierungsverzeichnis',
+ Timeout: 'Zeitlimit',
+ 'Max Retries': 'Maximale Wiederholungen',
+ 'Disable Cache Control': 'Cache-Steuerung deaktivieren',
+ 'Memory Discovery Max Dirs': 'Maximale Verzeichnisse für Speichererkennung',
+ 'Load Memory From Include Directories':
+ 'Speicher aus Include-Verzeichnissen laden',
+ 'Respect .gitignore': '.gitignore beachten',
+ 'Respect .qwenignore': '.qwenignore beachten',
+ 'Enable Recursive File Search': 'Rekursive Dateisuche aktivieren',
+ 'Disable Fuzzy Search': 'Unscharfe Suche deaktivieren',
+ 'Enable Interactive Shell': 'Interaktive Shell aktivieren',
+ 'Show Color': 'Farbe anzeigen',
+ 'Auto Accept': 'Automatisch akzeptieren',
+ 'Use Ripgrep': 'Ripgrep verwenden',
+ 'Use Builtin Ripgrep': 'Integriertes Ripgrep verwenden',
+ 'Enable Tool Output Truncation': 'Werkzeugausgabe-Kürzung aktivieren',
+ 'Tool Output Truncation Threshold': 'Schwellenwert für Werkzeugausgabe-Kürzung',
+ 'Tool Output Truncation Lines': 'Zeilen für Werkzeugausgabe-Kürzung',
+ 'Folder Trust': 'Ordnervertrauen',
+ 'Vision Model Preview': 'Vision-Modell-Vorschau',
+ 'Tool Schema Compliance': 'Werkzeug-Schema-Konformität',
+ // Settings enum options
+ 'Auto (detect from system)': 'Automatisch (vom System erkennen)',
+ Text: 'Text',
+ JSON: 'JSON',
+ Plan: 'Plan',
+ Default: 'Standard',
+ 'Auto Edit': 'Automatisch bearbeiten',
+ YOLO: 'YOLO',
+ 'toggle vim mode on/off': 'Vim-Modus ein-/ausschalten',
+ 'check session stats. Usage: /stats [model|tools]':
+ 'Sitzungsstatistiken prüfen. Verwendung: /stats [model|tools]',
+ 'Show model-specific usage statistics.':
+ 'Modellspezifische Nutzungsstatistiken anzeigen.',
+ 'Show tool-specific usage statistics.':
+ 'Werkzeugspezifische Nutzungsstatistiken anzeigen.',
+ 'exit the cli': 'CLI beenden',
+ 'list configured MCP servers and tools, or authenticate with OAuth-enabled servers':
+ 'Konfigurierte MCP-Server und Werkzeuge auflisten oder mit OAuth-fähigen Servern authentifizieren',
+ 'Manage workspace directories': 'Arbeitsbereichsverzeichnisse verwalten',
+ 'Add directories to the workspace. Use comma to separate multiple paths':
+ 'Verzeichnisse zum Arbeitsbereich hinzufügen. Komma zum Trennen mehrerer Pfade verwenden',
+ 'Show all directories in the workspace':
+ 'Alle Verzeichnisse im Arbeitsbereich anzeigen',
+ 'set external editor preference': 'Externen Editor festlegen',
+ 'Manage extensions': 'Erweiterungen verwalten',
+ 'List active extensions': 'Aktive Erweiterungen auflisten',
+ 'Update extensions. Usage: update |--all':
+ 'Erweiterungen aktualisieren. Verwendung: update |--all',
+ 'manage IDE integration': 'IDE-Integration verwalten',
+ 'check status of IDE integration': 'Status der IDE-Integration prüfen',
+ 'install required IDE companion for {{ideName}}':
+ 'Erforderlichen IDE-Begleiter für {{ideName}} installieren',
+ 'enable IDE integration': 'IDE-Integration aktivieren',
+ 'disable IDE integration': 'IDE-Integration deaktivieren',
+ 'IDE integration is not supported in your current environment. To use this feature, run Qwen Code in one of these supported IDEs: VS Code or VS Code forks.':
+ 'IDE-Integration wird in Ihrer aktuellen Umgebung nicht unterstützt. Um diese Funktion zu nutzen, führen Sie Qwen Code in einer dieser unterstützten IDEs aus: VS Code oder VS Code-Forks.',
+ 'Set up GitHub Actions': 'GitHub Actions einrichten',
+ 'Configure terminal keybindings for multiline input (VS Code, Cursor, Windsurf, Trae)':
+ 'Terminal-Tastenbelegungen für mehrzeilige Eingabe konfigurieren (VS Code, Cursor, Windsurf, Trae)',
+ 'Please restart your terminal for the changes to take effect.':
+ 'Bitte starten Sie Ihr Terminal neu, damit die Änderungen wirksam werden.',
+ 'Failed to configure terminal: {{error}}':
+ 'Fehler beim Konfigurieren des Terminals: {{error}}',
+ 'Could not determine {{terminalName}} config path on Windows: APPDATA environment variable is not set.':
+ 'Konnte {{terminalName}}-Konfigurationspfad unter Windows nicht ermitteln: APPDATA-Umgebungsvariable ist nicht gesetzt.',
+ '{{terminalName}} keybindings.json exists but is not a valid JSON array. Please fix the file manually or delete it to allow automatic configuration.':
+ '{{terminalName}} keybindings.json existiert, ist aber kein gültiges JSON-Array. Bitte korrigieren Sie die Datei manuell oder löschen Sie sie, um automatische Konfiguration zu ermöglichen.',
+ 'File: {{file}}': 'Datei: {{file}}',
+ 'Failed to parse {{terminalName}} keybindings.json. The file contains invalid JSON. Please fix the file manually or delete it to allow automatic configuration.':
+ 'Fehler beim Parsen von {{terminalName}} keybindings.json. Die Datei enthält ungültiges JSON. Bitte korrigieren Sie die Datei manuell oder löschen Sie sie, um automatische Konfiguration zu ermöglichen.',
+ 'Error: {{error}}': 'Fehler: {{error}}',
+ 'Shift+Enter binding already exists': 'Umschalt+Enter-Belegung existiert bereits',
+ 'Ctrl+Enter binding already exists': 'Strg+Enter-Belegung existiert bereits',
+ 'Existing keybindings detected. Will not modify to avoid conflicts.':
+ 'Bestehende Tastenbelegungen erkannt. Keine Änderungen, um Konflikte zu vermeiden.',
+ 'Please check and modify manually if needed: {{file}}':
+ 'Bitte prüfen und bei Bedarf manuell ändern: {{file}}',
+ 'Added Shift+Enter and Ctrl+Enter keybindings to {{terminalName}}.':
+ 'Umschalt+Enter und Strg+Enter Tastenbelegungen zu {{terminalName}} hinzugefügt.',
+ 'Modified: {{file}}': 'Geändert: {{file}}',
+ '{{terminalName}} keybindings already configured.':
+ '{{terminalName}}-Tastenbelegungen bereits konfiguriert.',
+ 'Failed to configure {{terminalName}}.':
+ 'Fehler beim Konfigurieren von {{terminalName}}.',
+ 'Your terminal is already configured for an optimal experience with multiline input (Shift+Enter and Ctrl+Enter).':
+ 'Ihr Terminal ist bereits für optimale Erfahrung mit mehrzeiliger Eingabe konfiguriert (Umschalt+Enter und Strg+Enter).',
+ 'Could not detect terminal type. Supported terminals: VS Code, Cursor, Windsurf, and Trae.':
+ 'Terminal-Typ konnte nicht erkannt werden. Unterstützte Terminals: VS Code, Cursor, Windsurf und Trae.',
+ 'Terminal "{{terminal}}" is not supported yet.':
+ 'Terminal "{{terminal}}" wird noch nicht unterstützt.',
+
+ // ============================================================================
+ // Commands - Language
+ // ============================================================================
+ 'Invalid language. Available: en-US, zh-CN':
+ 'Ungültige Sprache. Verfügbar: en-US, zh-CN',
+ 'Language subcommands do not accept additional arguments.':
+ 'Sprach-Unterbefehle akzeptieren keine zusätzlichen Argumente.',
+ 'Current UI language: {{lang}}': 'Aktuelle UI-Sprache: {{lang}}',
+ 'Current LLM output language: {{lang}}':
+ 'Aktuelle LLM-Ausgabesprache: {{lang}}',
+ 'LLM output language not set': 'LLM-Ausgabesprache nicht festgelegt',
+ 'Set UI language': 'UI-Sprache festlegen',
+ 'Set LLM output language': 'LLM-Ausgabesprache festlegen',
+ 'Usage: /language ui [zh-CN|en-US]': 'Verwendung: /language ui [zh-CN|en-US]',
+ 'Usage: /language output ': 'Verwendung: /language output ',
+ 'Example: /language output 中文': 'Beispiel: /language output Deutsch',
+ 'Example: /language output English': 'Beispiel: /language output English',
+ 'Example: /language output 日本語': 'Beispiel: /language output Japanisch',
+ 'UI language changed to {{lang}}': 'UI-Sprache geändert zu {{lang}}',
+ 'LLM output language rule file generated at {{path}}':
+ 'LLM-Ausgabesprach-Regeldatei generiert unter {{path}}',
+ 'Please restart the application for the changes to take effect.':
+ 'Bitte starten Sie die Anwendung neu, damit die Änderungen wirksam werden.',
+ 'Failed to generate LLM output language rule file: {{error}}':
+ 'Fehler beim Generieren der LLM-Ausgabesprach-Regeldatei: {{error}}',
+ 'Invalid command. Available subcommands:':
+ 'Ungültiger Befehl. Verfügbare Unterbefehle:',
+ 'Available subcommands:': 'Verfügbare Unterbefehle:',
+ 'To request additional UI language packs, please open an issue on GitHub.':
+ 'Um zusätzliche UI-Sprachpakete anzufordern, öffnen Sie bitte ein Issue auf GitHub.',
+ 'Available options:': 'Verfügbare Optionen:',
+ ' - zh-CN: Simplified Chinese': ' - zh-CN: Vereinfachtes Chinesisch',
+ ' - en-US: English': ' - en-US: Englisch',
+ 'Set UI language to Simplified Chinese (zh-CN)':
+ 'UI-Sprache auf Vereinfachtes Chinesisch (zh-CN) setzen',
+ 'Set UI language to English (en-US)': 'UI-Sprache auf Englisch (en-US) setzen',
+
+ // ============================================================================
+ // Commands - Approval Mode
+ // ============================================================================
+ 'Approval Mode': 'Genehmigungsmodus',
+ 'Current approval mode: {{mode}}': 'Aktueller Genehmigungsmodus: {{mode}}',
+ 'Available approval modes:': 'Verfügbare Genehmigungsmodi:',
+ 'Approval mode changed to: {{mode}}': 'Genehmigungsmodus geändert zu: {{mode}}',
+ 'Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})':
+ 'Genehmigungsmodus geändert zu: {{mode}} (gespeichert in {{scope}} Einstellungen{{location}})',
+ 'Usage: /approval-mode [--session|--user|--project]':
+ 'Verwendung: /approval-mode [--session|--user|--project]',
+
+ 'Scope subcommands do not accept additional arguments.':
+ 'Bereichs-Unterbefehle akzeptieren keine zusätzlichen Argumente.',
+ 'Plan mode - Analyze only, do not modify files or execute commands':
+ 'Planungsmodus - Nur analysieren, keine Dateien ändern oder Befehle ausführen',
+ 'Default mode - Require approval for file edits or shell commands':
+ 'Standardmodus - Genehmigung für Dateibearbeitungen oder Shell-Befehle erforderlich',
+ 'Auto-edit mode - Automatically approve file edits':
+ 'Automatischer Bearbeitungsmodus - Dateibearbeitungen automatisch genehmigen',
+ 'YOLO mode - Automatically approve all tools':
+ 'YOLO-Modus - Alle Werkzeuge automatisch genehmigen',
+ '{{mode}} mode': '{{mode}}-Modus',
+ 'Settings service is not available; unable to persist the approval mode.':
+ 'Einstellungsdienst nicht verfügbar; Genehmigungsmodus kann nicht gespeichert werden.',
+ 'Failed to save approval mode: {{error}}':
+ 'Fehler beim Speichern des Genehmigungsmodus: {{error}}',
+ 'Failed to change approval mode: {{error}}':
+ 'Fehler beim Ändern des Genehmigungsmodus: {{error}}',
+ 'Apply to current session only (temporary)':
+ 'Nur auf aktuelle Sitzung anwenden (temporär)',
+ 'Persist for this project/workspace': 'Für dieses Projekt/Arbeitsbereich speichern',
+ 'Persist for this user on this machine':
+ 'Für diesen Benutzer auf diesem Computer speichern',
+ 'Analyze only, do not modify files or execute commands':
+ 'Nur analysieren, keine Dateien ändern oder Befehle ausführen',
+ 'Require approval for file edits or shell commands':
+ 'Genehmigung für Dateibearbeitungen oder Shell-Befehle erforderlich',
+ 'Automatically approve file edits': 'Dateibearbeitungen automatisch genehmigen',
+ 'Automatically approve all tools': 'Alle Werkzeuge automatisch genehmigen',
+ 'Workspace approval mode exists and takes priority. User-level change will have no effect.':
+ 'Arbeitsbereich-Genehmigungsmodus existiert und hat Vorrang. Benutzerebene-Änderung hat keine Wirkung.',
+ '(Use Enter to select, Tab to change focus)':
+ '(Enter zum Auswählen, Tab zum Fokuswechsel)',
+ 'Apply To': 'Anwenden auf',
+ 'User Settings': 'Benutzereinstellungen',
+ 'Workspace Settings': 'Arbeitsbereich-Einstellungen',
+
+ // ============================================================================
+ // Commands - Memory
+ // ============================================================================
+ 'Commands for interacting with memory.':
+ 'Befehle für die Interaktion mit dem Speicher.',
+ 'Show the current memory contents.': 'Aktuellen Speicherinhalt anzeigen.',
+ 'Show project-level memory contents.': 'Projektebene-Speicherinhalt anzeigen.',
+ 'Show global memory contents.': 'Globalen Speicherinhalt anzeigen.',
+ 'Add content to project-level memory.':
+ 'Inhalt zum Projektebene-Speicher hinzufügen.',
+ 'Add content to global memory.': 'Inhalt zum globalen Speicher hinzufügen.',
+ 'Refresh the memory from the source.': 'Speicher aus der Quelle aktualisieren.',
+ 'Usage: /memory add --project ':
+ 'Verwendung: /memory add --project ',
+ 'Usage: /memory add --global ':
+ 'Verwendung: /memory add --global ',
+ 'Attempting to save to project memory: "{{text}}"':
+ 'Versuche im Projektspeicher zu speichern: "{{text}}"',
+ 'Attempting to save to global memory: "{{text}}"':
+ 'Versuche im globalen Speicher zu speichern: "{{text}}"',
+ 'Current memory content from {{count}} file(s):':
+ 'Aktueller Speicherinhalt aus {{count}} Datei(en):',
+ 'Memory is currently empty.': 'Speicher ist derzeit leer.',
+ 'Project memory file not found or is currently empty.':
+ 'Projektspeicherdatei nicht gefunden oder derzeit leer.',
+ 'Global memory file not found or is currently empty.':
+ 'Globale Speicherdatei nicht gefunden oder derzeit leer.',
+ 'Global memory is currently empty.': 'Globaler Speicher ist derzeit leer.',
+ 'Global memory content:\n\n---\n{{content}}\n---':
+ 'Globaler Speicherinhalt:\n\n---\n{{content}}\n---',
+ 'Project memory content from {{path}}:\n\n---\n{{content}}\n---':
+ 'Projektspeicherinhalt von {{path}}:\n\n---\n{{content}}\n---',
+ 'Project memory is currently empty.': 'Projektspeicher ist derzeit leer.',
+ 'Refreshing memory from source files...':
+ 'Speicher wird aus Quelldateien aktualisiert...',
+ 'Add content to the memory. Use --global for global memory or --project for project memory.':
+ 'Inhalt zum Speicher hinzufügen. --global für globalen Speicher oder --project für Projektspeicher verwenden.',
+ 'Usage: /memory add [--global|--project] ':
+ 'Verwendung: /memory add [--global|--project] ',
+ 'Attempting to save to memory {{scope}}: "{{fact}}"':
+ 'Versuche im Speicher {{scope}} zu speichern: "{{fact}}"',
+
+ // ============================================================================
+ // Commands - MCP
+ // ============================================================================
+ 'Authenticate with an OAuth-enabled MCP server':
+ 'Mit einem OAuth-fähigen MCP-Server authentifizieren',
+ 'List configured MCP servers and tools':
+ 'Konfigurierte MCP-Server und Werkzeuge auflisten',
+ 'Restarts MCP servers.': 'MCP-Server neu starten.',
+ 'Config not loaded.': 'Konfiguration nicht geladen.',
+ 'Could not retrieve tool registry.': 'Werkzeugregister konnte nicht abgerufen werden.',
+ 'No MCP servers configured with OAuth authentication.':
+ 'Keine MCP-Server mit OAuth-Authentifizierung konfiguriert.',
+ 'MCP servers with OAuth authentication:':
+ 'MCP-Server mit OAuth-Authentifizierung:',
+ 'Use /mcp auth to authenticate.':
+ 'Verwenden Sie /mcp auth zur Authentifizierung.',
+ "MCP server '{{name}}' not found.": "MCP-Server '{{name}}' nicht gefunden.",
+ "Successfully authenticated and refreshed tools for '{{name}}'.":
+ "Erfolgreich authentifiziert und Werkzeuge für '{{name}}' aktualisiert.",
+ "Failed to authenticate with MCP server '{{name}}': {{error}}":
+ "Authentifizierung mit MCP-Server '{{name}}' fehlgeschlagen: {{error}}",
+ "Re-discovering tools from '{{name}}'...":
+ "Werkzeuge von '{{name}}' werden neu erkannt...",
+
+ // ============================================================================
+ // Commands - Chat
+ // ============================================================================
+ 'Manage conversation history.': 'Gesprächsverlauf verwalten.',
+ 'List saved conversation checkpoints': 'Gespeicherte Gesprächsprüfpunkte auflisten',
+ 'No saved conversation checkpoints found.':
+ 'Keine gespeicherten Gesprächsprüfpunkte gefunden.',
+ 'List of saved conversations:': 'Liste gespeicherter Gespräche:',
+ 'Note: Newest last, oldest first': 'Hinweis: Neueste zuletzt, älteste zuerst',
+ 'Save the current conversation as a checkpoint. Usage: /chat save ':
+ 'Aktuelles Gespräch als Prüfpunkt speichern. Verwendung: /chat save ',
+ 'Missing tag. Usage: /chat save ':
+ 'Tag fehlt. Verwendung: /chat save ',
+ 'Delete a conversation checkpoint. Usage: /chat delete ':
+ 'Gesprächsprüfpunkt löschen. Verwendung: /chat delete ',
+ 'Missing tag. Usage: /chat delete ':
+ 'Tag fehlt. Verwendung: /chat delete ',
+ "Conversation checkpoint '{{tag}}' has been deleted.":
+ "Gesprächsprüfpunkt '{{tag}}' wurde gelöscht.",
+ "Error: No checkpoint found with tag '{{tag}}'.":
+ "Fehler: Kein Prüfpunkt mit Tag '{{tag}}' gefunden.",
+ 'Resume a conversation from a checkpoint. Usage: /chat resume ':
+ 'Gespräch von einem Prüfpunkt fortsetzen. Verwendung: /chat resume ',
+ 'Missing tag. Usage: /chat resume ':
+ 'Tag fehlt. Verwendung: /chat resume ',
+ 'No saved checkpoint found with tag: {{tag}}.':
+ 'Kein gespeicherter Prüfpunkt mit Tag gefunden: {{tag}}.',
+ 'A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?':
+ 'Ein Prüfpunkt mit dem Tag {{tag}} existiert bereits. Möchten Sie ihn überschreiben?',
+ 'No chat client available to save conversation.':
+ 'Kein Chat-Client verfügbar, um Gespräch zu speichern.',
+ 'Conversation checkpoint saved with tag: {{tag}}.':
+ 'Gesprächsprüfpunkt gespeichert mit Tag: {{tag}}.',
+ 'No conversation found to save.': 'Kein Gespräch zum Speichern gefunden.',
+ 'No chat client available to share conversation.':
+ 'Kein Chat-Client verfügbar, um Gespräch zu teilen.',
+ 'Invalid file format. Only .md and .json are supported.':
+ 'Ungültiges Dateiformat. Nur .md und .json werden unterstützt.',
+ 'Error sharing conversation: {{error}}':
+ 'Fehler beim Teilen des Gesprächs: {{error}}',
+ 'Conversation shared to {{filePath}}': 'Gespräch geteilt nach {{filePath}}',
+ 'No conversation found to share.': 'Kein Gespräch zum Teilen gefunden.',
+ 'Share the current conversation to a markdown or json file. Usage: /chat share ':
+ 'Aktuelles Gespräch in eine Markdown- oder JSON-Datei teilen. Verwendung: /chat share ',
+
+ // ============================================================================
+ // Commands - Summary
+ // ============================================================================
+ 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md':
+ 'Projektzusammenfassung generieren und in .qwen/PROJECT_SUMMARY.md speichern',
+ 'No chat client available to generate summary.':
+ 'Kein Chat-Client verfügbar, um Zusammenfassung zu generieren.',
+ 'Already generating summary, wait for previous request to complete':
+ 'Zusammenfassung wird bereits generiert, warten Sie auf Abschluss der vorherigen Anfrage',
+ 'No conversation found to summarize.': 'Kein Gespräch zum Zusammenfassen gefunden.',
+ 'Failed to generate project context summary: {{error}}':
+ 'Fehler beim Generieren der Projektkontextzusammenfassung: {{error}}',
+ 'Saved project summary to {{filePathForDisplay}}.':
+ 'Projektzusammenfassung gespeichert unter {{filePathForDisplay}}.',
+ 'Saving project summary...': 'Projektzusammenfassung wird gespeichert...',
+ 'Generating project summary...': 'Projektzusammenfassung wird generiert...',
+ 'Failed to generate summary - no text content received from LLM response':
+ 'Fehler beim Generieren der Zusammenfassung - kein Textinhalt von LLM-Antwort erhalten',
+
+ // ============================================================================
+ // Commands - Model
+ // ============================================================================
+ 'Switch the model for this session': 'Modell für diese Sitzung wechseln',
+ 'Content generator configuration not available.':
+ 'Inhaltsgenerator-Konfiguration nicht verfügbar.',
+ 'Authentication type not available.': 'Authentifizierungstyp nicht verfügbar.',
+ 'No models available for the current authentication type ({{authType}}).':
+ 'Keine Modelle für den aktuellen Authentifizierungstyp ({{authType}}) verfügbar.',
+
+ // ============================================================================
+ // Commands - Clear
+ // ============================================================================
+ 'Starting a new session, resetting chat, and clearing terminal.':
+ 'Neue Sitzung wird gestartet, Chat wird zurückgesetzt und Terminal wird gelöscht.',
+ 'Starting a new session and clearing.':
+ 'Neue Sitzung wird gestartet und gelöscht.',
+
+ // ============================================================================
+ // Commands - Compress
+ // ============================================================================
+ 'Already compressing, wait for previous request to complete':
+ 'Komprimierung läuft bereits, warten Sie auf Abschluss der vorherigen Anfrage',
+ 'Failed to compress chat history.': 'Fehler beim Komprimieren des Chatverlaufs.',
+ 'Failed to compress chat history: {{error}}':
+ 'Fehler beim Komprimieren des Chatverlaufs: {{error}}',
+ 'Compressing chat history': 'Chatverlauf wird komprimiert',
+ 'Chat history compressed from {{originalTokens}} to {{newTokens}} tokens.':
+ 'Chatverlauf komprimiert von {{originalTokens}} auf {{newTokens}} Token.',
+ 'Compression was not beneficial for this history size.':
+ 'Komprimierung war für diese Verlaufsgröße nicht vorteilhaft.',
+ 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.':
+ 'Chatverlauf-Komprimierung hat die Größe nicht reduziert. Dies kann auf Probleme mit dem Komprimierungs-Prompt hindeuten.',
+ 'Could not compress chat history due to a token counting error.':
+ 'Chatverlauf konnte aufgrund eines Token-Zählfehlers nicht komprimiert werden.',
+ 'Chat history is already compressed.': 'Chatverlauf ist bereits komprimiert.',
+
+ // ============================================================================
+ // Commands - Directory
+ // ============================================================================
+ 'Configuration is not available.': 'Konfiguration ist nicht verfügbar.',
+ 'Please provide at least one path to add.':
+ 'Bitte geben Sie mindestens einen Pfad zum Hinzufügen an.',
+ 'The /directory add command is not supported in restrictive sandbox profiles. Please use --include-directories when starting the session instead.':
+ 'Der Befehl /directory add wird in restriktiven Sandbox-Profilen nicht unterstützt. Bitte verwenden Sie --include-directories beim Starten der Sitzung.',
+ "Error adding '{{path}}': {{error}}": "Fehler beim Hinzufügen von '{{path}}': {{error}}",
+ 'Successfully added QWEN.md files from the following directories if there are:\n- {{directories}}':
+ 'QWEN.md-Dateien aus folgenden Verzeichnissen erfolgreich hinzugefügt, falls vorhanden:\n- {{directories}}',
+ 'Error refreshing memory: {{error}}': 'Fehler beim Aktualisieren des Speichers: {{error}}',
+ 'Successfully added directories:\n- {{directories}}':
+ 'Verzeichnisse erfolgreich hinzugefügt:\n- {{directories}}',
+ 'Current workspace directories:\n{{directories}}':
+ 'Aktuelle Arbeitsbereichsverzeichnisse:\n{{directories}}',
+
+ // ============================================================================
+ // Commands - Docs
+ // ============================================================================
+ 'Please open the following URL in your browser to view the documentation:\n{{url}}':
+ 'Bitte öffnen Sie folgende URL in Ihrem Browser, um die Dokumentation anzusehen:\n{{url}}',
+ 'Opening documentation in your browser: {{url}}':
+ 'Dokumentation wird in Ihrem Browser geöffnet: {{url}}',
+
+ // ============================================================================
+ // Dialogs - Tool Confirmation
+ // ============================================================================
+ 'Do you want to proceed?': 'Möchten Sie fortfahren?',
+ 'Yes, allow once': 'Ja, einmal erlauben',
+ 'Allow always': 'Immer erlauben',
+ No: 'Nein',
+ 'No (esc)': 'Nein (Esc)',
+ 'Yes, allow always for this session': 'Ja, für diese Sitzung immer erlauben',
+ 'Modify in progress:': 'Änderung in Bearbeitung:',
+ 'Save and close external editor to continue':
+ 'Speichern und externen Editor schließen, um fortzufahren',
+ 'Apply this change?': 'Diese Änderung anwenden?',
+ 'Yes, allow always': 'Ja, immer erlauben',
+ 'Modify with external editor': 'Mit externem Editor bearbeiten',
+ 'No, suggest changes (esc)': 'Nein, Änderungen vorschlagen (Esc)',
+ "Allow execution of: '{{command}}'?": "Ausführung erlauben von: '{{command}}'?",
+ 'Yes, allow always ...': 'Ja, immer erlauben ...',
+ 'Yes, and auto-accept edits': 'Ja, und Änderungen automatisch akzeptieren',
+ 'Yes, and manually approve edits': 'Ja, und Änderungen manuell genehmigen',
+ 'No, keep planning (esc)': 'Nein, weiter planen (Esc)',
+ 'URLs to fetch:': 'Abzurufende URLs:',
+ 'MCP Server: {{server}}': 'MCP-Server: {{server}}',
+ 'Tool: {{tool}}': 'Werkzeug: {{tool}}',
+ 'Allow execution of MCP tool "{{tool}}" from server "{{server}}"?':
+ 'Ausführung des MCP-Werkzeugs "{{tool}}" von Server "{{server}}" erlauben?',
+ 'Yes, always allow tool "{{tool}}" from server "{{server}}"':
+ 'Ja, Werkzeug "{{tool}}" von Server "{{server}}" immer erlauben',
+ 'Yes, always allow all tools from server "{{server}}"':
+ 'Ja, alle Werkzeuge von Server "{{server}}" immer erlauben',
+
+ // ============================================================================
+ // Dialogs - Shell Confirmation
+ // ============================================================================
+ 'Shell Command Execution': 'Shell-Befehlsausführung',
+ 'A custom command wants to run the following shell commands:':
+ 'Ein benutzerdefinierter Befehl möchte folgende Shell-Befehle ausführen:',
+
+ // ============================================================================
+ // Dialogs - Pro Quota
+ // ============================================================================
+ 'Pro quota limit reached for {{model}}.':
+ 'Pro-Kontingentlimit für {{model}} erreicht.',
+ 'Change auth (executes the /auth command)':
+ 'Authentifizierung ändern (führt den /auth-Befehl aus)',
+ 'Continue with {{model}}': 'Mit {{model}} fortfahren',
+
+ // ============================================================================
+ // Dialogs - Welcome Back
+ // ============================================================================
+ 'Current Plan:': 'Aktueller Plan:',
+ 'Progress: {{done}}/{{total}} tasks completed':
+ 'Fortschritt: {{done}}/{{total}} Aufgaben abgeschlossen',
+ ', {{inProgress}} in progress': ', {{inProgress}} in Bearbeitung',
+ 'Pending Tasks:': 'Ausstehende Aufgaben:',
+ 'What would you like to do?': 'Was möchten Sie tun?',
+ 'Choose how to proceed with your session:':
+ 'Wählen Sie, wie Sie mit Ihrer Sitzung fortfahren möchten:',
+ 'Start new chat session': 'Neue Chat-Sitzung starten',
+ 'Continue previous conversation': 'Vorheriges Gespräch fortsetzen',
+ '👋 Welcome back! (Last updated: {{timeAgo}})':
+ '👋 Willkommen zurück! (Zuletzt aktualisiert: {{timeAgo}})',
+ '🎯 Overall Goal:': '🎯 Gesamtziel:',
+
+ // ============================================================================
+ // Dialogs - Auth
+ // ============================================================================
+ 'Get started': 'Loslegen',
+ 'How would you like to authenticate for this project?':
+ 'Wie möchten Sie sich für dieses Projekt authentifizieren?',
+ 'OpenAI API key is required to use OpenAI authentication.':
+ 'OpenAI API-Schlüssel ist für die OpenAI-Authentifizierung erforderlich.',
+ 'You must select an auth method to proceed. Press Ctrl+C again to exit.':
+ 'Sie müssen eine Authentifizierungsmethode wählen, um fortzufahren. Drücken Sie erneut Strg+C zum Beenden.',
+ '(Use Enter to Set Auth)': '(Enter zum Festlegen der Authentifizierung)',
+ 'Terms of Services and Privacy Notice for Qwen Code':
+ 'Nutzungsbedingungen und Datenschutzhinweis für Qwen Code',
+ 'Qwen OAuth': 'Qwen OAuth',
+ OpenAI: 'OpenAI',
+ 'Failed to login. Message: {{message}}':
+ 'Anmeldung fehlgeschlagen. Meldung: {{message}}',
+ 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.':
+ 'Authentifizierung ist auf {{enforcedType}} festgelegt, aber Sie verwenden derzeit {{currentType}}.',
+ 'Qwen OAuth authentication timed out. Please try again.':
+ 'Qwen OAuth-Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.',
+ 'Qwen OAuth authentication cancelled.':
+ 'Qwen OAuth-Authentifizierung abgebrochen.',
+ 'Qwen OAuth Authentication': 'Qwen OAuth-Authentifizierung',
+ 'Please visit this URL to authorize:': 'Bitte besuchen Sie diese URL zur Autorisierung:',
+ 'Or scan the QR code below:': 'Oder scannen Sie den QR-Code unten:',
+ 'Waiting for authorization': 'Warten auf Autorisierung',
+ 'Time remaining:': 'Verbleibende Zeit:',
+ '(Press ESC or CTRL+C to cancel)': '(ESC oder STRG+C zum Abbrechen drücken)',
+ 'Qwen OAuth Authentication Timeout': 'Qwen OAuth-Authentifizierung abgelaufen',
+ 'OAuth token expired (over {{seconds}} seconds). Please select authentication method again.':
+ 'OAuth-Token abgelaufen (über {{seconds}} Sekunden). Bitte wählen Sie erneut eine Authentifizierungsmethode.',
+ 'Press any key to return to authentication type selection.':
+ 'Drücken Sie eine beliebige Taste, um zur Authentifizierungstypauswahl zurückzukehren.',
+ 'Waiting for Qwen OAuth authentication...':
+ 'Warten auf Qwen OAuth-Authentifizierung...',
+ 'Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.':
+ 'Hinweis: Ihr bestehender API-Schlüssel in settings.json wird bei Verwendung von Qwen OAuth nicht gelöscht. Sie können später bei Bedarf zur OpenAI-Authentifizierung zurückwechseln.',
+ 'Authentication timed out. Please try again.':
+ 'Authentifizierung abgelaufen. Bitte versuchen Sie es erneut.',
+ 'Waiting for auth... (Press ESC or CTRL+C to cancel)':
+ 'Warten auf Authentifizierung... (ESC oder STRG+C zum Abbrechen drücken)',
+ 'Failed to authenticate. Message: {{message}}':
+ 'Authentifizierung fehlgeschlagen. Meldung: {{message}}',
+ 'Authenticated successfully with {{authType}} credentials.':
+ 'Erfolgreich mit {{authType}}-Anmeldedaten authentifiziert.',
+ 'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}':
+ 'Ungültiger QWEN_DEFAULT_AUTH_TYPE-Wert: "{{value}}". Gültige Werte sind: {{validValues}}',
+ 'OpenAI Configuration Required': 'OpenAI-Konfiguration erforderlich',
+ 'Please enter your OpenAI configuration. You can get an API key from':
+ 'Bitte geben Sie Ihre OpenAI-Konfiguration ein. Sie können einen API-Schlüssel erhalten von',
+ 'API Key:': 'API-Schlüssel:',
+ 'Invalid credentials: {{errorMessage}}':
+ 'Ungültige Anmeldedaten: {{errorMessage}}',
+ 'Failed to validate credentials': 'Anmeldedaten konnten nicht validiert werden',
+ 'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel':
+ 'Enter zum Fortfahren, Tab/↑↓ zum Navigieren, Esc zum Abbrechen',
+
+ // ============================================================================
+ // Dialogs - Model
+ // ============================================================================
+ 'Select Model': 'Modell auswählen',
+ '(Press Esc to close)': '(Esc zum Schließen drücken)',
+ 'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)':
+ 'Das neueste Qwen Coder Modell von Alibaba Cloud ModelStudio (Version: qwen3-coder-plus-2025-09-23)',
+ 'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)':
+ 'Das neueste Qwen Vision Modell von Alibaba Cloud ModelStudio (Version: qwen3-vl-plus-2025-09-23)',
+
+ // ============================================================================
+ // Dialogs - Permissions
+ // ============================================================================
+ 'Manage folder trust settings': 'Ordnervertrauenseinstellungen verwalten',
+
+ // ============================================================================
+ // Status Bar
+ // ============================================================================
+ 'Using:': 'Verwendet:',
+ '{{count}} open file': '{{count}} geöffnete Datei',
+ '{{count}} open files': '{{count}} geöffnete Dateien',
+ '(ctrl+g to view)': '(Strg+G zum Anzeigen)',
+ '{{count}} {{name}} file': '{{count}} {{name}}-Datei',
+ '{{count}} {{name}} files': '{{count}} {{name}}-Dateien',
+ '{{count}} MCP server': '{{count}} MCP-Server',
+ '{{count}} MCP servers': '{{count}} MCP-Server',
+ '{{count}} Blocked': '{{count}} blockiert',
+ '(ctrl+t to view)': '(Strg+T zum Anzeigen)',
+ '(ctrl+t to toggle)': '(Strg+T zum Umschalten)',
+ 'Press Ctrl+C again to exit.': 'Drücken Sie erneut Strg+C zum Beenden.',
+ 'Press Ctrl+D again to exit.': 'Drücken Sie erneut Strg+D zum Beenden.',
+ 'Press Esc again to clear.': 'Drücken Sie erneut Esc zum Löschen.',
+
+ // ============================================================================
+ // MCP Status
+ // ============================================================================
+ 'No MCP servers configured.': 'Keine MCP-Server konfiguriert.',
+ 'Please view MCP documentation in your browser:':
+ 'Bitte sehen Sie die MCP-Dokumentation in Ihrem Browser:',
+ 'or use the cli /docs command': 'oder verwenden Sie den CLI-Befehl /docs',
+ '⏳ MCP servers are starting up ({{count}} initializing)...':
+ '⏳ MCP-Server werden gestartet ({{count}} werden initialisiert)...',
+ 'Note: First startup may take longer. Tool availability will update automatically.':
+ 'Hinweis: Der erste Start kann länger dauern. Werkzeugverfügbarkeit wird automatisch aktualisiert.',
+ 'Configured MCP servers:': 'Konfigurierte MCP-Server:',
+ Ready: 'Bereit',
+ 'Starting... (first startup may take longer)':
+ 'Wird gestartet... (erster Start kann länger dauern)',
+ Disconnected: 'Getrennt',
+ '{{count}} tool': '{{count}} Werkzeug',
+ '{{count}} tools': '{{count}} Werkzeuge',
+ '{{count}} prompt': '{{count}} Prompt',
+ '{{count}} prompts': '{{count}} Prompts',
+ '(from {{extensionName}})': '(von {{extensionName}})',
+ OAuth: 'OAuth',
+ 'OAuth expired': 'OAuth abgelaufen',
+ 'OAuth not authenticated': 'OAuth nicht authentifiziert',
+ 'tools and prompts will appear when ready':
+ 'Werkzeuge und Prompts werden angezeigt, wenn bereit',
+ '{{count}} tools cached': '{{count}} Werkzeuge zwischengespeichert',
+ 'Tools:': 'Werkzeuge:',
+ 'Parameters:': 'Parameter:',
+ 'Prompts:': 'Prompts:',
+ Blocked: 'Blockiert',
+ '💡 Tips:': '💡 Tipps:',
+ Use: 'Verwenden',
+ 'to show server and tool descriptions':
+ 'um Server- und Werkzeugbeschreibungen anzuzeigen',
+ 'to show tool parameter schemas': 'um Werkzeug-Parameter-Schemas anzuzeigen',
+ 'to hide descriptions': 'um Beschreibungen auszublenden',
+ 'to authenticate with OAuth-enabled servers':
+ 'um sich bei OAuth-fähigen Servern zu authentifizieren',
+ Press: 'Drücken Sie',
+ 'to toggle tool descriptions on/off':
+ 'um Werkzeugbeschreibungen ein-/auszuschalten',
+ "Starting OAuth authentication for MCP server '{{name}}'...":
+ "OAuth-Authentifizierung für MCP-Server '{{name}}' wird gestartet...",
+ 'Restarting MCP servers...': 'MCP-Server werden neu gestartet...',
+
+ // ============================================================================
+ // Startup Tips
+ // ============================================================================
+ 'Tips for getting started:': 'Tipps zum Einstieg:',
+ '1. Ask questions, edit files, or run commands.':
+ '1. Stellen Sie Fragen, bearbeiten Sie Dateien oder führen Sie Befehle aus.',
+ '2. Be specific for the best results.':
+ '2. Seien Sie spezifisch für die besten Ergebnisse.',
+ 'files to customize your interactions with Qwen Code.':
+ 'Dateien, um Ihre Interaktionen mit Qwen Code anzupassen.',
+ 'for more information.': 'für weitere Informationen.',
+
+ // ============================================================================
+ // Exit Screen / Stats
+ // ============================================================================
+ 'Agent powering down. Goodbye!': 'Agent wird heruntergefahren. Auf Wiedersehen!',
+ 'To continue this session, run': 'Um diese Sitzung fortzusetzen, führen Sie aus',
+ 'Interaction Summary': 'Interaktionszusammenfassung',
+ 'Session ID:': 'Sitzungs-ID:',
+ 'Tool Calls:': 'Werkzeugaufrufe:',
+ 'Success Rate:': 'Erfolgsrate:',
+ 'User Agreement:': 'Benutzerzustimmung:',
+ reviewed: 'überprüft',
+ 'Code Changes:': 'Codeänderungen:',
+ Performance: 'Leistung',
+ 'Wall Time:': 'Gesamtzeit:',
+ 'Agent Active:': 'Agent aktiv:',
+ 'API Time:': 'API-Zeit:',
+ 'Tool Time:': 'Werkzeugzeit:',
+ 'Session Stats': 'Sitzungsstatistiken',
+ 'Model Usage': 'Modellnutzung',
+ Reqs: 'Anfragen',
+ 'Input Tokens': 'Eingabe-Token',
+ 'Output Tokens': 'Ausgabe-Token',
+ 'Savings Highlight:': 'Einsparungen:',
+ 'of input tokens were served from the cache, reducing costs.':
+ 'der Eingabe-Token wurden aus dem Cache bedient, was die Kosten reduziert.',
+ 'Tip: For a full token breakdown, run `/stats model`.':
+ 'Tipp: Für eine vollständige Token-Aufschlüsselung führen Sie `/stats model` aus.',
+ 'Model Stats For Nerds': 'Modellstatistiken für Nerds',
+ 'Tool Stats For Nerds': 'Werkzeugstatistiken für Nerds',
+ Metric: 'Metrik',
+ API: 'API',
+ Requests: 'Anfragen',
+ Errors: 'Fehler',
+ 'Avg Latency': 'Durchschn. Latenz',
+ Tokens: 'Token',
+ Total: 'Gesamt',
+ Prompt: 'Prompt',
+ Cached: 'Zwischengespeichert',
+ Thoughts: 'Gedanken',
+ Tool: 'Werkzeug',
+ Output: 'Ausgabe',
+ 'No API calls have been made in this session.':
+ 'In dieser Sitzung wurden keine API-Aufrufe gemacht.',
+ 'Tool Name': 'Werkzeugname',
+ Calls: 'Aufrufe',
+ 'Success Rate': 'Erfolgsrate',
+ 'Avg Duration': 'Durchschn. Dauer',
+ 'User Decision Summary': 'Benutzerentscheidungs-Zusammenfassung',
+ 'Total Reviewed Suggestions:': 'Insgesamt überprüfter Vorschläge:',
+ ' » Accepted:': ' » Akzeptiert:',
+ ' » Rejected:': ' » Abgelehnt:',
+ ' » Modified:': ' » Geändert:',
+ ' Overall Agreement Rate:': ' Gesamtzustimmungsrate:',
+ 'No tool calls have been made in this session.':
+ 'In dieser Sitzung wurden keine Werkzeugaufrufe gemacht.',
+ 'Session start time is unavailable, cannot calculate stats.':
+ 'Sitzungsstartzeit nicht verfügbar, Statistiken können nicht berechnet werden.',
+
+ // ============================================================================
+ // Loading Phrases
+ // ============================================================================
+ 'Waiting for user confirmation...': 'Warten auf Benutzerbestätigung...',
+ '(esc to cancel, {{time}})': '(Esc zum Abbrechen, {{time}})',
+
+ // ============================================================================
+ // Loading Phrases
+ // ============================================================================
+ WITTY_LOADING_PHRASES: [
+ 'Auf gut Glück!',
+ 'Genialität wird ausgeliefert...',
+ 'Die Serifen werden aufgemalt...',
+ 'Durch den Schleimpilz navigieren...',
+ 'Die digitalen Geister werden befragt...',
+ 'Splines werden retikuliert...',
+ 'Die KI-Hamster werden aufgewärmt...',
+ 'Die Zaubermuschel wird befragt...',
+ 'Witzige Erwiderung wird generiert...',
+ 'Die Algorithmen werden poliert...',
+ 'Perfektion braucht Zeit (mein Code auch)...',
+ 'Frische Bytes werden gebrüht...',
+ 'Elektronen werden gezählt...',
+ 'Kognitive Prozessoren werden aktiviert...',
+ 'Auf Syntaxfehler im Universum wird geprüft...',
+ 'Einen Moment, Humor wird optimiert...',
+ 'Pointen werden gemischt...',
+ 'Neuronale Netze werden entwirrt...',
+ 'Brillanz wird kompiliert...',
+ 'wit.exe wird geladen...',
+ 'Die Wolke der Weisheit wird beschworen...',
+ 'Eine witzige Antwort wird vorbereitet...',
+ 'Einen Moment, ich debugge die Realität...',
+ 'Die Optionen werden verwirrt...',
+ 'Kosmische Frequenzen werden eingestellt...',
+ 'Eine Antwort wird erstellt, die Ihrer Geduld würdig ist...',
+ 'Die Einsen und Nullen werden kompiliert...',
+ 'Abhängigkeiten werden aufgelöst... und existenzielle Krisen...',
+ 'Erinnerungen werden defragmentiert... sowohl RAM als auch persönliche...',
+ 'Das Humor-Modul wird neu gestartet...',
+ 'Das Wesentliche wird zwischengespeichert (hauptsächlich Katzen-Memes)...',
+ 'Für lächerliche Geschwindigkeit wird optimiert',
+ 'Bits werden getauscht... sagen Sie es nicht den Bytes...',
+ 'Garbage Collection läuft... bin gleich zurück...',
+ 'Das Internet wird zusammengebaut...',
+ 'Kaffee wird in Code umgewandelt...',
+ 'Die Syntax der Realität wird aktualisiert...',
+ 'Die Synapsen werden neu verdrahtet...',
+ 'Ein verlegtes Semikolon wird gesucht...',
+ 'Die Zahnräder werden geschmiert...',
+ 'Die Server werden vorgeheizt...',
+ 'Der Fluxkompensator wird kalibriert...',
+ 'Der Unwahrscheinlichkeitsantrieb wird aktiviert...',
+ 'Die Macht wird kanalisiert...',
+ 'Die Sterne werden für optimale Antwort ausgerichtet...',
+ 'So sagen wir alle...',
+ 'Die nächste große Idee wird geladen...',
+ 'Einen Moment, ich bin in der Zone...',
+ 'Bereite mich vor, Sie mit Brillanz zu blenden...',
+ 'Einen Augenblick, ich poliere meinen Witz...',
+ 'Halten Sie durch, ich erschaffe ein Meisterwerk...',
+ 'Einen Moment, ich debugge das Universum...',
+ 'Einen Moment, ich richte die Pixel aus...',
+ 'Einen Moment, ich optimiere den Humor...',
+ 'Einen Moment, ich tune die Algorithmen...',
+ 'Warp-Geschwindigkeit aktiviert...',
+ 'Mehr Dilithium-Kristalle werden gesucht...',
+ 'Keine Panik...',
+ 'Dem weißen Kaninchen wird gefolgt...',
+ 'Die Wahrheit ist hier drin... irgendwo...',
+ 'Auf die Kassette wird gepustet...',
+ 'Ladevorgang... Machen Sie eine Fassrolle!',
+ 'Auf den Respawn wird gewartet...',
+ 'Der Kessel-Flug wird in weniger als 12 Parsec beendet...',
+ 'Der Kuchen ist keine Lüge, er lädt nur noch...',
+ 'Am Charaktererstellungsbildschirm wird herumgefummelt...',
+ 'Einen Moment, ich suche das richtige Meme...',
+ "'A' wird zum Fortfahren gedrückt...",
+ 'Digitale Katzen werden gehütet...',
+ 'Die Pixel werden poliert...',
+ 'Ein passender Ladebildschirm-Witz wird gesucht...',
+ 'Ich lenke Sie mit diesem witzigen Spruch ab...',
+ 'Fast da... wahrscheinlich...',
+ 'Unsere Hamster arbeiten so schnell sie können...',
+ 'Cloudy wird am Kopf gestreichelt...',
+ 'Die Katze wird gestreichelt...',
+ 'Meinen Chef rickrollen...',
+ 'Never gonna give you up, never gonna let you down...',
+ 'Auf den Bass wird geschlagen...',
+ 'Die Schnozbeeren werden probiert...',
+ "I'm going the distance, I'm going for speed...",
+ 'Ist dies das wahre Leben? Ist dies nur Fantasie?...',
+ 'Ich habe ein gutes Gefühl dabei...',
+ 'Den Bären wird gestupst...',
+ 'Recherche zu den neuesten Memes...',
+ 'Überlege, wie ich das witziger machen kann...',
+ 'Hmmm... lassen Sie mich nachdenken...',
+ 'Wie nennt man einen Fisch ohne Augen? Ein Fsh...',
+ 'Warum ging der Computer zur Therapie? Er hatte zu viele Bytes...',
+ 'Warum mögen Programmierer keine Natur? Sie hat zu viele Bugs...',
+ 'Warum bevorzugen Programmierer den Dunkelmodus? Weil Licht Bugs anzieht...',
+ 'Warum ging der Entwickler pleite? Er hat seinen ganzen Cache aufgebraucht...',
+ 'Was kann man mit einem kaputten Bleistift machen? Nichts, er ist sinnlos...',
+ 'Perkussive Wartung wird angewendet...',
+ 'Die richtige USB-Ausrichtung wird gesucht...',
+ 'Es wird sichergestellt, dass der magische Rauch in den Kabeln bleibt...',
+ 'Versuche Vim zu beenden...',
+ 'Das Hamsterrad wird angeworfen...',
+ 'Das ist kein Bug, das ist ein undokumentiertes Feature...',
+ 'Engage.',
+ 'Ich komme wieder... mit einer Antwort.',
+ 'Mein anderer Prozess ist eine TARDIS...',
+ 'Mit dem Maschinengeist wird kommuniziert...',
+ 'Die Gedanken marinieren lassen...',
+ 'Gerade erinnert, wo ich meine Schlüssel hingelegt habe...',
+ 'Über die Kugel wird nachgedacht...',
+ 'Ich habe Dinge gesehen, die Sie nicht glauben würden... wie einen Benutzer, der Lademeldungen liest.',
+ 'Nachdenklicher Blick wird initiiert...',
+ 'Was ist der Lieblingssnack eines Computers? Mikrochips.',
+ 'Warum tragen Java-Entwickler Brillen? Weil sie nicht C#.',
+ 'Der Laser wird aufgeladen... pew pew!',
+ 'Durch Null wird geteilt... nur Spaß!',
+ 'Suche nach einem erwachsenen Aufseh... ich meine, Verarbeitung.',
+ 'Es piept und boopt.',
+ 'Pufferung... weil auch KIs einen Moment brauchen.',
+ 'Quantenteilchen werden für schnellere Antwort verschränkt...',
+ 'Das Chrom wird poliert... an den Algorithmen.',
+ 'Sind Sie nicht unterhalten? (Arbeite daran!)',
+ 'Die Code-Gremlins werden beschworen... zum Helfen, natürlich.',
+ 'Warte nur auf das Einwahlton-Ende...',
+ 'Das Humor-O-Meter wird neu kalibriert.',
+ 'Mein anderer Ladebildschirm ist noch lustiger.',
+ 'Ziemlich sicher, dass irgendwo eine Katze über die Tastatur läuft...',
+ 'Verbessern... Verbessern... Lädt noch.',
+ 'Das ist kein Bug, das ist ein Feature... dieses Ladebildschirms.',
+ 'Haben Sie versucht, es aus- und wieder einzuschalten? (Den Ladebildschirm, nicht mich.)',
+ 'Zusätzliche Pylonen werden gebaut...',
+ ],
+};
diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js
index fb9475426..5e8b16629 100644
--- a/packages/cli/src/i18n/locales/en.js
+++ b/packages/cli/src/i18n/locales/en.js
@@ -89,6 +89,9 @@ 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',
@@ -1037,7 +1040,6 @@ 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...",
diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js
index ee583e0f9..9685c104b 100644
--- a/packages/cli/src/i18n/locales/ru.js
+++ b/packages/cli/src/i18n/locales/ru.js
@@ -89,6 +89,10 @@ 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': 'Изменение темы',
@@ -1056,7 +1060,6 @@ export default {
'Провожу настройку методом тыка...',
'Ищем, какой стороной вставлять флешку...',
'Следим, чтобы волшебный дым не вышел из проводов...',
- 'Переписываем всё на Rust без особой причины...',
'Пытаемся выйти из Vim...',
'Раскручиваем колесо для хомяка...',
'Это не баг, а фича...',
diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js
index 5c5d21679..c3550f7e8 100644
--- a/packages/cli/src/i18n/locales/zh.js
+++ b/packages/cli/src/i18n/locales/zh.js
@@ -88,6 +88,9 @@ 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': '选择主题',
diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts
index 0ba94cbb2..be04b7f2b 100644
--- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts
+++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.test.ts
@@ -630,6 +630,67 @@ 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', () => {
diff --git a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts
index ef6655370..072497000 100644
--- a/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts
+++ b/packages/cli/src/nonInteractive/io/BaseJsonOutputAdapter.ts
@@ -816,9 +816,18 @@ export abstract class BaseJsonOutputAdapter {
parentToolUseId?: string | null,
): void {
const actualParentToolUseId = parentToolUseId ?? null;
- const fragment = [subject?.trim(), description?.trim()]
- .filter((value) => value && value.length > 0)
- .join(': ');
+
+ // 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(': ');
if (!fragment) {
return;
}
diff --git a/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts b/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts
index d0bd23255..ff3aa1f5d 100644
--- a/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts
+++ b/packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.test.ts
@@ -323,6 +323,68 @@ 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,
diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts
index 07fd168fc..840ba69d5 100644
--- a/packages/cli/src/nonInteractiveCli.test.ts
+++ b/packages/cli/src/nonInteractiveCli.test.ts
@@ -298,7 +298,9 @@ describe('runNonInteractive', () => {
mockConfig,
expect.objectContaining({ name: 'testTool' }),
expect.any(AbortSignal),
- undefined,
+ expect.objectContaining({
+ outputUpdateHandler: expect.any(Function),
+ }),
);
// Verify first call has isContinuation: false
expect(mockGeminiClient.sendMessageStream).toHaveBeenNthCalledWith(
@@ -771,6 +773,52 @@ 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();
@@ -1777,4 +1825,84 @@ 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');
+ });
});
diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts
index 067f190b9..4088c9283 100644
--- a/packages/cli/src/nonInteractiveCli.ts
+++ b/packages/cli/src/nonInteractiveCli.ts
@@ -4,7 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core';
+import type {
+ Config,
+ ToolCallRequestInfo,
+ ToolResultDisplay,
+} from '@qwen-code/qwen-code-core';
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
@@ -308,6 +312,8 @@ export async function runNonInteractive(
config.getContentGeneratorConfig()?.authType,
);
process.stderr.write(`${errorText}\n`);
+ // Throw error to exit with non-zero code
+ throw new Error(errorText);
}
}
}
@@ -333,7 +339,7 @@ export async function runNonInteractive(
? options.controlService.permission.getToolCallUpdateCallback()
: undefined;
- // Only pass outputUpdateHandler for Task tool
+ // Create output handler for Task tool (for subagent execution)
const isTaskTool = finalRequestInfo.name === 'task';
const taskToolProgress = isTaskTool
? createTaskToolProgressHandler(
@@ -343,20 +349,41 @@ 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,
- isTaskTool && taskToolProgressHandler
+ outputUpdateHandler || toolCallUpdateCallback
? {
- outputUpdateHandler: taskToolProgressHandler,
- onToolCallsUpdate: toolCallUpdateCallback,
- }
- : toolCallUpdateCallback
- ? {
+ ...(outputUpdateHandler && { outputUpdateHandler }),
+ ...(toolCallUpdateCallback && {
onToolCallsUpdate: toolCallUpdateCallback,
- }
- : undefined,
+ }),
+ }
+ : undefined,
);
// Note: In JSON mode, subagent messages are automatically added to the main
diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts
index cbfca4d2d..151faf324 100644
--- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts
+++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts
@@ -72,6 +72,7 @@ describe('ShellProcessor', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getShouldUseNodePtyShell: vi.fn().mockReturnValue(false),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
+ getAllowedTools: vi.fn().mockReturnValue([]),
};
context = createMockCommandContext({
@@ -196,6 +197,35 @@ 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(
diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.ts b/packages/cli/src/services/prompt-processors/shellProcessor.ts
index c10526e62..2a6df7161 100644
--- a/packages/cli/src/services/prompt-processors/shellProcessor.ts
+++ b/packages/cli/src/services/prompt-processors/shellProcessor.ts
@@ -7,11 +7,13 @@
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';
@@ -124,6 +126,15 @@ 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) {
@@ -132,10 +143,17 @@ export class ShellProcessor implements IPromptProcessor {
);
}
- // If not a hard denial, respect YOLO mode and auto-approve.
- if (config.getApprovalMode() !== ApprovalMode.YOLO) {
- disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
+ // 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;
+ }
+
+ disallowedCommands.forEach((uc) => commandsToConfirm.add(uc));
}
}
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 7921b2039..38dad449c 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -925,7 +925,12 @@ export const AppContainer = (props: AppContainerProps) => {
const handleIdePromptComplete = useCallback(
(result: IdeIntegrationNudgeResult) => {
if (result.userSelection === 'yes') {
- handleSlashCommand('/ide install');
+ // Check whether the extension has been pre-installed
+ if (result.isExtensionPreInstalled) {
+ handleSlashCommand('/ide enable');
+ } else {
+ handleSlashCommand('/ide install');
+ }
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
} else if (result.userSelection === 'dismiss') {
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx
index 8ab350064..9cd6d5311 100644
--- a/packages/cli/src/ui/IdeIntegrationNudge.tsx
+++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx
@@ -38,6 +38,7 @@ 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'] &&
@@ -70,13 +71,15 @@ export function IdeIntegrationNudge({
},
];
- 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'
- }.`;
+ 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'
+ }.`;
return (
{
let mockContext: CommandContext;
+ let mockSetApprovalMode: ReturnType;
beforeEach(() => {
+ mockSetApprovalMode = vi.fn();
mockContext = createMockCommandContext({
services: {
config: {
getApprovalMode: () => 'default',
- setApprovalMode: () => {},
+ setApprovalMode: mockSetApprovalMode,
},
- settings: {
- merged: {},
- setValue: () => {},
- forScope: () => ({}),
- } as unknown as LoadedSettings,
},
});
});
@@ -41,7 +38,7 @@ describe('approvalModeCommand', () => {
expect(approvalModeCommand.kind).toBe(CommandKind.BUILT_IN);
});
- it('should open approval mode dialog when invoked', async () => {
+ it('should open approval mode dialog when invoked without arguments', async () => {
const result = (await approvalModeCommand.action?.(
mockContext,
'',
@@ -51,16 +48,123 @@ describe('approvalModeCommand', () => {
expect(result.dialog).toBe('approval-mode');
});
- it('should open approval mode dialog with arguments (ignored)', async () => {
+ it('should open approval mode dialog when invoked with whitespace only', 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();
});
diff --git a/packages/cli/src/ui/commands/approvalModeCommand.ts b/packages/cli/src/ui/commands/approvalModeCommand.ts
index 90ae774bf..f41e4b1cf 100644
--- a/packages/cli/src/ui/commands/approvalModeCommand.ts
+++ b/packages/cli/src/ui/commands/approvalModeCommand.ts
@@ -8,9 +8,25 @@ 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',
@@ -19,10 +35,49 @@ export const approvalModeCommand: SlashCommand = {
},
kind: CommandKind.BUILT_IN,
action: async (
- _context: CommandContext,
- _args: string,
- ): Promise => ({
- type: 'dialog',
- dialog: 'approval-mode',
- }),
+ context: CommandContext,
+ args: string,
+ ): Promise => {
+ 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 }),
+ };
+ },
};
diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts
index ebb7e3dc7..be3a12bc9 100644
--- a/packages/cli/src/ui/commands/ideCommand.ts
+++ b/packages/cli/src/ui/commands/ideCommand.ts
@@ -191,11 +191,23 @@ export const ideCommand = async (): Promise => {
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: `No installer is available for ${ideClient.getDetectedIdeDisplayName()}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
+ text: `Automatic installation is not supported for ${ideName}. Please install the '${QWEN_CODE_COMPANION_EXTENSION_NAME}' extension manually from the marketplace.`,
},
Date.now(),
);
diff --git a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx
index bac7f23df..13a176c62 100644
--- a/packages/cli/src/ui/components/StandaloneSessionPicker.tsx
+++ b/packages/cli/src/ui/components/StandaloneSessionPicker.tsx
@@ -87,7 +87,13 @@ export async function showResumeSessionPicker(
let selectedId: string | undefined;
const { unmount, waitUntilExit } = render(
-
+
{
diff --git a/packages/cli/src/utils/errors.test.ts b/packages/cli/src/utils/errors.test.ts
index 818c3ac39..e3a27bd42 100644
--- a/packages/cli/src/utils/errors.test.ts
+++ b/packages/cli/src/utils/errors.test.ts
@@ -6,7 +6,11 @@
import { vi, type Mock, type MockInstance } from 'vitest';
import type { Config } from '@qwen-code/qwen-code-core';
-import { OutputFormat, FatalInputError } from '@qwen-code/qwen-code-core';
+import {
+ OutputFormat,
+ FatalInputError,
+ ToolErrorType,
+} from '@qwen-code/qwen-code-core';
import {
getErrorMessage,
handleError,
@@ -65,6 +69,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
describe('errors', () => {
let mockConfig: Config;
let processExitSpy: MockInstance;
+ let processStderrWriteSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
@@ -74,6 +79,11 @@ 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}`);
@@ -84,11 +94,13 @@ 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();
});
@@ -432,6 +444,87 @@ 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
+ ).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
+ ).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
+ ).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
+ ).mockReturnValue(OutputFormat.TEXT);
+
+ handleToolError(
+ toolName,
+ toolError,
+ mockConfig,
+ ToolErrorType.FILE_NOT_FOUND,
+ );
+
+ expect(processStderrWriteSpy).not.toHaveBeenCalled();
+ expect(processExitSpy).not.toHaveBeenCalled();
+ });
+ });
});
describe('handleCancellationError', () => {
diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts
index 5338fa2fd..f804a630c 100644
--- a/packages/cli/src/utils/errors.ts
+++ b/packages/cli/src/utils/errors.ts
@@ -11,6 +11,7 @@ import {
parseAndFormatApiError,
FatalTurnLimitedError,
FatalCancellationError,
+ ToolErrorType,
} from '@qwen-code/qwen-code-core';
export function getErrorMessage(error: unknown): string {
@@ -102,10 +103,24 @@ export function handleToolError(
toolName: string,
toolError: Error,
config: Config,
- _errorCode?: string | number,
+ errorCode?: string | number,
resultDisplay?: string,
): void {
- // Always just log to stderr; JSON/streaming formatting happens in the tool_result block elsewhere
+ // 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
if (config.getDebugMode()) {
console.error(
`Error executing tool ${toolName}: ${resultDisplay || toolError.message}`,
diff --git a/packages/core/package.json b/packages/core/package.json
index de06c78bc..e7baa13b2 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
- "version": "0.6.0",
+ "version": "0.6.1",
"description": "Qwen Code Core",
"repository": {
"type": "git",
diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts
index aeffdfc78..c7e2806ac 100644
--- a/packages/core/src/core/coreToolScheduler.ts
+++ b/packages/core/src/core/coreToolScheduler.ts
@@ -824,7 +824,6 @@ export class CoreToolScheduler {
*/
const shouldAutoDeny =
!this.config.isInteractive() &&
- !this.config.getIdeMode() &&
!this.config.getExperimentalZedIntegration() &&
this.config.getInputFormat() !== InputFormat.STREAM_JSON;
diff --git a/packages/core/src/core/openaiContentGenerator/converter.ts b/packages/core/src/core/openaiContentGenerator/converter.ts
index 07a8f1831..5ec7a4935 100644
--- a/packages/core/src/core/openaiContentGenerator/converter.ts
+++ b/packages/core/src/core/openaiContentGenerator/converter.ts
@@ -752,6 +752,8 @@ 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
@@ -769,6 +771,7 @@ export class OpenAIContentConverter {
candidatesTokenCount: finalCompletionTokens,
totalTokenCount: totalTokens,
cachedContentTokenCount: cachedTokens,
+ thoughtsTokenCount: thinkingTokens,
};
}
diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts
index 88ac38f6a..ef27a7798 100644
--- a/packages/core/src/core/openaiContentGenerator/pipeline.ts
+++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts
@@ -317,15 +317,22 @@ export class ContentGenerationPipeline {
}
private buildReasoningConfig(): Record {
- const reasoning = this.contentGeneratorConfig.reasoning;
+ // 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 model’s default behavior.
- if (reasoning === false) {
- return {};
- }
+ // We plan to introduce provider- and model-specific settings to enable more
+ // fine-grained control over reasoning configuration.
- return {
- reasoning_effort: reasoning?.effort ?? 'medium',
- };
+ return {};
}
/**
diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts
index c56069503..521a6768c 100644
--- a/packages/core/src/core/openaiContentGenerator/provider/default.ts
+++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts
@@ -58,8 +58,6 @@ export class DefaultOpenAICompatibleProvider
}
getDefaultGenerationConfig(): GenerateContentConfig {
- return {
- topP: 0.95,
- };
+ return {};
}
}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 56680403b..7f7bd115b 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -38,6 +38,7 @@ 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';
diff --git a/packages/core/src/services/shellExecutionService.test.ts b/packages/core/src/services/shellExecutionService.test.ts
index 4dfc48918..e63fba28d 100644
--- a/packages/core/src/services/shellExecutionService.test.ts
+++ b/packages/core/src/services/shellExecutionService.test.ts
@@ -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(undefined);
+ expect(handle.pid).toBe(12345);
expect(onOutputEventMock).toHaveBeenCalledWith({
type: 'data',
@@ -829,7 +829,7 @@ describe('ShellExecutionService child_process fallback', () => {
[],
expect.objectContaining({
shell: true,
- detached: false,
+ detached: true,
}),
);
});
diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts
index be0c26ff7..c870b5f4e 100644
--- a/packages/core/src/services/shellExecutionService.ts
+++ b/packages/core/src/services/shellExecutionService.ts
@@ -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 } from 'node:child_process';
+import { spawn as cpSpawn, spawnSync } from 'node:child_process';
import { TextDecoder } from 'node:util';
import os from 'node:os';
import type { IPty } from '@lydell/node-pty';
@@ -98,6 +98,48 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
return lines.join('\n').trimEnd();
};
+interface ProcessCleanupStrategy {
+ killPty(pid: number, pty: ActivePty): void;
+ killChildProcesses(pids: Set): 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.
@@ -106,6 +148,29 @@ const getFullBufferText = (terminal: pkg.Terminal): string => {
export class ShellExecutionService {
private static activePtys = new Map();
+ private static activeChildProcesses = new Set();
+
+ 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.
*
@@ -164,7 +229,7 @@ export class ShellExecutionService {
stdio: ['ignore', 'pipe', 'pipe'],
windowsVerbatimArguments: true,
shell: isWindows ? true : 'bash',
- detached: !isWindows,
+ detached: true,
env: {
...process.env,
QWEN_CODE: '1',
@@ -281,9 +346,13 @@ 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.activePtys.delete(child.pid);
+ this.activeChildProcesses.delete(child.pid);
}
handleExit(code, signal);
});
@@ -310,7 +379,7 @@ export class ShellExecutionService {
}
});
- return { pid: undefined, result };
+ return { pid: child.pid, result };
} catch (e) {
const error = e as Error;
return {
diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts
index 8c98045dd..8b6788a70 100644
--- a/packages/core/src/tools/shell.test.ts
+++ b/packages/core/src/tools/shell.test.ts
@@ -248,7 +248,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir',
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -275,7 +275,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -300,7 +300,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -325,7 +325,7 @@ describe('ShellTool', () => {
wrappedCommand,
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -350,7 +350,7 @@ describe('ShellTool', () => {
wrappedCommand,
'/test/dir/subdir',
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -378,7 +378,7 @@ describe('ShellTool', () => {
'dir',
'/test/dir',
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -471,7 +471,7 @@ describe('ShellTool', () => {
expect(summarizer.summarizeToolOutput).toHaveBeenCalledWith(
expect.any(String),
mockConfig.getGeminiClient(),
- mockAbortSignal,
+ expect.any(AbortSignal),
1000,
);
expect(result.llmContent).toBe('summarized output');
@@ -580,7 +580,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -610,7 +610,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -640,7 +640,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -699,7 +699,7 @@ describe('ShellTool', () => {
expect.stringContaining('npm install'),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -728,7 +728,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit'),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -758,7 +758,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -794,7 +794,7 @@ describe('ShellTool', () => {
expect.stringContaining('git commit -m "Initial commit"'),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -831,7 +831,7 @@ describe('ShellTool', () => {
),
expect.any(String),
expect.any(Function),
- mockAbortSignal,
+ expect.any(AbortSignal),
false,
{},
);
@@ -962,4 +962,41 @@ 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,
+ {},
+ );
+ });
+ });
});
diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts
index 5354f9251..d7afae599 100644
--- a/packages/core/src/tools/shell.ts
+++ b/packages/core/src/tools/shell.ts
@@ -143,11 +143,24 @@ export class ShellToolInvocation extends BaseToolInvocation<
const shouldRunInBackground = this.params.is_background;
let finalCommand = processedCommand;
- // If explicitly marked as background and doesn't already end with &, add it
- if (shouldRunInBackground && !finalCommand.trim().endsWith('&')) {
+ // 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('&')
+ ) {
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
@@ -169,10 +182,6 @@ export class ShellToolInvocation extends BaseToolInvocation<
commandToExecute,
cwd,
(event: ShellOutputEvent) => {
- if (!updateOutput) {
- return;
- }
-
let shouldUpdate = false;
switch (event.type) {
@@ -201,7 +210,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
}
- if (shouldUpdate) {
+ if (shouldUpdate && updateOutput) {
updateOutput(
typeof cumulativeOutput === 'string'
? cumulativeOutput
@@ -219,6 +228,21 @@ 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 to stop)'
+ : ' (Use kill to stop)';
+
+ return {
+ llmContent: `Background command started.${pidMsg}${killHint}`,
+ returnDisplay: `Background command started.${pidMsg}${killHint}`,
+ };
+ }
+
const result = await resultPromise;
const backgroundPIDs: number[] = [];
diff --git a/packages/sdk-java/.editorconfig b/packages/sdk-java/.editorconfig
new file mode 100644
index 000000000..53a4241f9
--- /dev/null
+++ b/packages/sdk-java/.editorconfig
@@ -0,0 +1,24 @@
+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
diff --git a/packages/sdk-java/.gitignore b/packages/sdk-java/.gitignore
new file mode 100644
index 000000000..23cdb8c94
--- /dev/null
+++ b/packages/sdk-java/.gitignore
@@ -0,0 +1,14 @@
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+# Mac
+.DS_Store
+
+# Maven
+log/
+target/
+
+/docs/
diff --git a/packages/sdk-java/LICENSE b/packages/sdk-java/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/packages/sdk-java/LICENSE
@@ -0,0 +1,201 @@
+ 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.
diff --git a/packages/sdk-java/QWEN.md b/packages/sdk-java/QWEN.md
new file mode 100644
index 000000000..4fedee46f
--- /dev/null
+++ b/packages/sdk-java/QWEN.md
@@ -0,0 +1,378 @@
+# 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, 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
diff --git a/packages/sdk-java/README.md b/packages/sdk-java/README.md
new file mode 100644
index 000000000..772c93742
--- /dev/null
+++ b/packages/sdk-java/README.md
@@ -0,0 +1,312 @@
+# 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
+
+ com.alibaba
+ qwencode-sdk
+ {$version}
+
+```
+
+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 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() {{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 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, 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.
diff --git a/packages/sdk-java/checkstyle.xml b/packages/sdk-java/checkstyle.xml
new file mode 100644
index 000000000..c67c1319f
--- /dev/null
+++ b/packages/sdk-java/checkstyle.xml
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/sdk-java/pom.xml b/packages/sdk-java/pom.xml
new file mode 100644
index 000000000..6a5fae4f4
--- /dev/null
+++ b/packages/sdk-java/pom.xml
@@ -0,0 +1,193 @@
+
+ 4.0.0
+ com.alibaba
+ qwencode-sdk
+ jar
+ 0.0.1-alpha
+ qwencode-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.
+
+ https://maven.apache.org
+
+
+ Apache 2
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+ A business-friendly OSS license
+
+
+
+ https://github.com/QwenLM/qwen-code
+ scm:git:https://github.com/QwenLM/qwen-code.git
+
+
+ 1.8
+ 1.8
+ UTF-8
+ 3.6.0
+ 0.8.12
+ 5.14.1
+ 1.3.16
+ 2.0.60
+ 3.13.0
+ 0.8.0
+ 2.2.1
+ 2.9.1
+ 1.5
+
+
+
+
+
+ org.junit
+ junit-bom
+ pom
+ ${junit5.version}
+ import
+
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback-classic.version}
+
+
+ org.apache.commons
+ commons-lang3
+ 3.20.0
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ ${fastjson2.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${checkstyle-maven-plugin.version}
+
+ checkstyle.xml
+
+
+
+
+ check
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco-maven-plugin.version}
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ ${central-publishing-maven-plugin.version}
+ true
+
+ central
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ ${maven-source-plugin.version}
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${maven-javadoc-plugin.version}
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ ${maven-gpg-plugin.version}
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+ Alibaba Group
+ https://github.com/alibaba
+
+
+
+ skyfire
+ skyfire
+ gengwei.gw(at)alibaba-inc.com
+
+ Developer
+ Designer
+
+ +8
+ https://github.com/gwinthis
+
+
+
+
+
+ central
+ https://central.sonatype.com/repository/maven-snapshots/
+
+
+ central
+ https://central.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java
new file mode 100644
index 000000000..9a654034d
--- /dev/null
+++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/QwenCodeCli.java
@@ -0,0 +1,142 @@
+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 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 simpleQuery(String prompt, TransportOptions transportOptions) {
+ final List 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;
+ }
+}
diff --git a/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java
new file mode 100644
index 000000000..ba0356545
--- /dev/null
+++ b/packages/sdk-java/src/main/java/com/alibaba/qwen/code/cli/protocol/data/AssistantContent.java
@@ -0,0 +1,95 @@
+package com.alibaba.qwen.code.cli.protocol.data;
+
+import java.util.Map;
+
+/**
+ * Represents content from the assistant in a Qwen Code session.
+ *
+ * @param The type of content
+ * @author skyfire
+ * @version $Id: 0.0.1
+ */
+public interface AssistantContent {
+ /**
+ * 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 {
+ /**
+ * Gets the text content.
+ *
+ * @return The text content
+ */
+ String getText();
+ }
+
+ /**
+ * Represents thinking content from the assistant.
+ */
+ interface ThingkingAssistantContent extends AssistantContent {
+ /**
+ * Gets the thinking content.
+ *
+ * @return The thinking content
+ */
+ String getThinking();
+ }
+
+ /**
+ * Represents tool use content from the assistant.
+ */
+ interface ToolUseAssistantContent extends AssistantContent