Compare commits

...

24 Commits

Author SHA1 Message Date
pomelo-nwu
bfe8133ea3 feat: refactor docs 2025-12-05 10:51:57 +08:00
pomelo-nwu
17785c418d feat: restructure docs 2025-12-04 18:26:05 +08:00
tanzhenxin
6729980b47 skip acp integration test in sandbox env (#1141) 2025-12-04 10:28:21 +08:00
tanzhenxin
2ca36d7508 skip one flaky integration test (#1137) 2025-12-03 19:40:14 +08:00
tanzhenxin
e426c15e9e pump version to 0.4.0 (#1132) 2025-12-03 18:10:11 +08:00
tanzhenxin
0a75d85ac9 Session-Level Conversation History Management (#1113) 2025-12-03 18:04:48 +08:00
Zijun Yang
a7abd8d09f fix(shell-utils): resolve command detection on Ubuntu by using shell for builtins (#1123) 2025-12-02 11:49:40 +08:00
Mingholy
c9af74816a fix: reset authType settings (#1091)
* fix: reset authType settings

* fix: failed json-output tests

* fix: sandbox exception log to stderr
2025-11-23 17:59:35 +08:00
pomelo
9cfea73207 Merge pull request #1097 from QwenLM/chore-action
fix(ci): remove non-existent label from release failure issue creation
2025-11-23 08:27:34 +08:00
pomelo-nwu
87b1ffe017 fix(ci): remove non-existent label from release failure issue creation 2025-11-22 14:23:49 +08:00
pomelo
83fc321e15 Merge pull request #1090 from QwenLM/feat/logger-enhancement
Improve Usage Statistics by Moving Key Snapshot Fields into Properties
2025-11-21 15:55:26 +08:00
pomelo
48b77541c3 feat(i18n): Add Internationalization Support for UI and LLM Output (#1058) 2025-11-21 15:44:37 +08:00
tanzhenxin
f2439f8d53 fix: skip one unstable test case 2025-11-21 15:43:05 +08:00
tanzhenxin
fb6d0b43fa feat: change shortcut for subagent execution display 2025-11-21 15:42:17 +08:00
tanzhenxin
627283d357 feat: enhance usage statistics in qwen logger 2025-11-21 15:17:34 +08:00
tanzhenxin
640f30655d chore: pump version to 0.3.0 (#1085) 2025-11-21 09:37:38 +08:00
Kdump
9e5387f159 Headless enhancement: add stream-json as input-format/output-format to support programmatically use (#926) 2025-11-21 09:26:05 +08:00
tanzhenxin
442a9aed58 Replace spawn with execFile for memory-safe command execution (#1068) 2025-11-20 15:04:00 +08:00
Mingholy
a15b84e2a1 refactor(auth): enhance useAuthCommand to include history management and improve error handling in QwenOAuth2Client (#1077) 2025-11-20 14:37:39 +08:00
tanzhenxin
07069f00d1 feat: remove prompt completion feature (#1076) 2025-11-20 14:36:51 +08:00
pomelo
e1e7a0d606 Merge pull request #1074 from cwtuan/patch-1
fix: remove broken link
2025-11-20 14:33:14 +08:00
cwtuan
fc638851e7 fix: remove broken link 2025-11-20 12:50:06 +08:00
citlalinda
e1f793b2e0 fix: character encoding corruption when executing the /copy command on Windows. (#1069)
Co-authored-by: linda <hxn@163.com>
2025-11-20 10:23:17 +08:00
tanzhenxin
3c64f7bff5 chore: pump version to 0.2.3 (#1073) 2025-11-20 10:09:12 +08:00
353 changed files with 29613 additions and 7471 deletions

View File

@@ -224,5 +224,4 @@ jobs:
run: |-
gh issue create \
--title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \
--label "kind/bug,release-failure"
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}"

11
.vscode/launch.json vendored
View File

@@ -73,7 +73,16 @@
"request": "launch",
"name": "Launch CLI Non-Interactive",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start", "--", "-p", "${input:prompt}", "-y"],
"runtimeArgs": [
"run",
"start",
"--",
"-p",
"${input:prompt}",
"-y",
"--output-format",
"stream-json"
],
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",

View File

@@ -25,7 +25,7 @@
</div>
Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) ([details](./README.gemini.md)), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance.
Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance.
## 💡 Free Options Available

View File

@@ -1,10 +1,14 @@
export default {
index: 'Welcome to Qwen Code',
cli: 'CLI',
core: 'Core',
tools: 'Tools',
features: 'Features',
'ide-integration': 'IDE Integration',
development: 'Development',
support: 'Support',
index: {
type: 'page',
display: 'hidden',
},
users: {
type: 'page',
title: 'User Guide',
},
developers: {
type: 'page',
title: 'Developer Guide',
},
};

23
docs/developers/_meta.ts Normal file
View File

@@ -0,0 +1,23 @@
export default {
'Contribute to Qwen Code': {
title: 'Contribute to Qwen Code',
type: 'separator',
},
architecture: 'Architecture',
contributing: 'Contributing Guide',
roadmap: 'Roadmap',
'Qwen Code SDK': {
title: 'Qwen Code SDK',
type: 'separator',
},
'Dive Into Qwen Code': {
title: 'Dive Into Qwen Code',
type: 'separator',
},
cli: {
display: 'hidden',
},
core: 'Core',
tools: 'Tools',
// development: 'Development',
};

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 350 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 381 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -5,12 +5,7 @@ export default {
commands: 'Commands',
configuration: 'Configuration',
'configuration-v1': 'Configuration (v1)',
themes: 'Themes',
tutorials: 'Tutorials',
'keyboard-shortcuts': 'Keyboard Shortcuts',
'trusted-folders': 'Trusted Folders',
'qwen-ignore': 'Ignoring Files',
Uninstall: 'Uninstall',
};
/**

View File

@@ -11,31 +11,8 @@ Slash commands provide meta-level control over the CLI itself.
- **`/bug`**
- **Description:** File an issue about Qwen Code. By default, the issue is filed within the GitHub repository for Qwen Code. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `advanced.bugCommand` setting in your `.qwen/settings.json` files.
- **`/chat`**
- **Description:** Save and resume conversation history for branching conversation state interactively, or resuming a previous state from a later session.
- **Sub-commands:**
- **`save`**
- **Description:** Saves the current conversation history. You must add a `<tag>` for identifying the conversation state.
- **Usage:** `/chat save <tag>`
- **Details on Checkpoint Location:** The default locations for saved chat checkpoints are:
- Linux/macOS: `~/.qwen/tmp/<project_hash>/`
- Windows: `C:\Users\<YourUsername>\.qwen\tmp\<project_hash>\`
- When you run `/chat list`, the CLI only scans these specific directories to find available checkpoints.
- **Note:** These checkpoints are for manually saving and resuming conversation states. For automatic checkpoints created before file modifications, see the [Checkpointing documentation](../checkpointing.md).
- **`resume`**
- **Description:** Resumes a conversation from a previous save.
- **Usage:** `/chat resume <tag>`
- **`list`**
- **Description:** Lists available tags for chat state resumption.
- **`delete`**
- **Description:** Deletes a saved conversation checkpoint.
- **Usage:** `/chat delete <tag>`
- **`share`**
- **Description** Writes the current conversation to a provided Markdown or JSON file.
- **Usage** `/chat share file.md` or `/chat share file.json`. If no filename is provided, then the CLI will generate one.
- **`/clear`**
- **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared.
- **`/clear`** (aliases: `reset`, `new`)
- **Description:** Clear conversation history and free up context by starting a fresh session. Also clears the terminal output and scrollback within the CLI.
- **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
- **`/summary`**
@@ -195,6 +172,16 @@ Slash commands provide meta-level control over the CLI itself.
- **`/init`**
- **Description:** Analyzes the current directory and creates a `QWEN.md` context file by default (or the filename specified by `contextFileName`). If a non-empty file already exists, no changes are made. The command seeds an empty file and prompts the model to populate it with project-specific instructions.
- [**`/language`**](./language.md)
- **Description:** View or change the language setting for both UI and LLM output.
- **Sub-commands:**
- **`ui`**: Set the UI language (zh-CN or en-US)
- **`output`**: Set the LLM output language
- **Usage:** `/language [ui|output] [language]`
- **Examples:**
- `/language ui zh-CN` (set UI language to Simplified Chinese)
- `/language output English` (set LLM output language to English)
### Custom Commands
For a quick start, see the [example](#example-a-pure-function-refactoring-command) below.

View File

@@ -671,4 +671,4 @@ Note: When usage statistics are enabled, events are sent to an Alibaba Cloud RUM
- **Category:** UI
- **Requires Restart:** No
- **Example:** `"enableWelcomeBack": false`
- **Details:** When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. This feature integrates with the `/chat summary` command and quit confirmation dialog. See the [Welcome Back documentation](./welcome-back.md) for more details.
- **Details:** When enabled, Qwen Code will automatically detect if you're returning to a project with a previously generated project summary (`.qwen/PROJECT_SUMMARY.md`) and show a dialog allowing you to continue your previous conversation or start fresh. This feature integrates with the `/summary` command and quit confirmation dialog. See the [Welcome Back documentation](./welcome-back.md) for more details.

View File

@@ -548,12 +548,31 @@ Arguments passed directly when running the CLI can override other configurations
- The prompt is processed within the interactive session, not before it.
- Cannot be used when piping input from stdin.
- Example: `qwen -i "explain this code"`
- **`--output-format <format>`**:
- **`--continue`**:
- Resume the most recent session for the current project (current working directory).
- Works in interactive and headless modes (e.g., `qwen --continue -p "Keep going"`).
- **`--resume [sessionId]`**:
- Resume a specific session for the current project. When called without an ID, an interactive picker lists only this project's sessions with prompt preview, timestamps, message count, and optional git branch.
- If an ID is provided and not found for this project, the CLI exits with an error.
- **`--output-format <format>`** (**`-o <format>`**):
- **Description:** Specifies the format of the CLI output for non-interactive mode.
- **Values:**
- `text`: (Default) The standard human-readable output.
- `json`: A machine-readable JSON output.
- **Note:** For structured output and scripting, use the `--output-format json` flag.
- `json`: A machine-readable JSON output emitted at the end of execution.
- `stream-json`: Streaming JSON messages emitted as they occur during execution.
- **Note:** For structured output and scripting, use the `--output-format json` or `--output-format stream-json` flag. See [Headless Mode](../features/headless.md) for detailed information.
- **`--input-format <format>`**:
- **Description:** Specifies the format consumed from standard input.
- **Values:**
- `text`: (Default) Standard text input from stdin or command-line arguments.
- `stream-json`: JSON message protocol via stdin for bidirectional communication.
- **Requirement:** `--input-format stream-json` requires `--output-format stream-json` to be set.
- **Note:** When using `stream-json`, stdin is reserved for protocol messages. See [Headless Mode](../features/headless.md) for detailed information.
- **`--include-partial-messages`**:
- **Description:** Include partial assistant messages when using `stream-json` output format. When enabled, emits stream events (message_start, content_block_delta, etc.) as they occur during streaming.
- **Default:** `false`
- **Requirement:** Requires `--output-format stream-json` to be set.
- **Note:** See [Headless Mode](../features/headless.md) for detailed information about stream events.
- **`--sandbox`** (**`-s`**):
- Enables sandbox mode for this session.
- **`--sandbox-image`**:

View File

@@ -0,0 +1,71 @@
# Language Command
The `/language` command allows you to customize the language settings for both the Qwen Code user interface (UI) and the language model's output. This command supports two distinct functionalities:
1. Setting the UI language for the Qwen Code interface
2. Setting the output language for the language model (LLM)
## UI Language Settings
To change the UI language of Qwen Code, use the `ui` subcommand:
```
/language ui [zh-CN|en-US]
```
### Available UI Languages
- **zh-CN**: Simplified Chinese (简体中文)
- **en-US**: English
### Examples
```
/language ui zh-CN # Set UI language to Simplified Chinese
/language ui en-US # Set UI language to English
```
### UI Language Subcommands
You can also use direct subcommands for convenience:
- `/language ui zh-CN` or `/language ui zh` or `/language ui 中文`
- `/language ui en-US` or `/language ui en` or `/language ui english`
## LLM Output Language Settings
To set the language for the language model's responses, use the `output` subcommand:
```
/language output <language>
```
This command generates a language rule file that instructs the LLM to respond in the specified language. The rule file is saved to `~/.qwen/output-language.md`.
### Examples
```
/language output 中文 # Set LLM output language to Chinese
/language output English # Set LLM output language to English
/language output 日本語 # Set LLM output language to Japanese
```
## Viewing Current Settings
When used without arguments, the `/language` command displays the current language settings:
```
/language
```
This will show:
- Current UI language
- Current LLM output language (if set)
- Available subcommands
## Notes
- UI language changes take effect immediately and reload all command descriptions
- LLM output language settings are persisted in a rule file that is automatically included in the model's context
- To request additional UI language packs, please open an issue on GitHub

View File

View File

@@ -1,7 +1,5 @@
export default {
architecture: 'Architecture',
npm: 'NPM',
deployment: 'Deployment',
telemetry: 'Telemetry',
'integration-tests': 'Integration Tests',
'issue-and-pr-automation': 'Issue and PR Automation',

View File

@@ -0,0 +1 @@
# Qwen Code RoadMap

View File

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

View File

@@ -1,308 +0,0 @@
# Headless Mode
Headless mode allows you to run Qwen Code programmatically from command line
scripts and automation tools without any interactive UI. This is ideal for
scripting, automation, CI/CD pipelines, and building AI-powered tools.
- [Headless Mode](#headless-mode)
- [Overview](#overview)
- [Basic Usage](#basic-usage)
- [Direct Prompts](#direct-prompts)
- [Stdin Input](#stdin-input)
- [Combining with File Input](#combining-with-file-input)
- [Output Formats](#output-formats)
- [Text Output (Default)](#text-output-default)
- [JSON Output](#json-output)
- [Response Schema](#response-schema)
- [Example Usage](#example-usage)
- [File Redirection](#file-redirection)
- [Configuration Options](#configuration-options)
- [Examples](#examples)
- [Code review](#code-review)
- [Generate commit messages](#generate-commit-messages)
- [API documentation](#api-documentation)
- [Batch code analysis](#batch-code-analysis)
- [Code review](#code-review-1)
- [Log analysis](#log-analysis)
- [Release notes generation](#release-notes-generation)
- [Model and tool usage tracking](#model-and-tool-usage-tracking)
- [Resources](#resources)
## Overview
The headless mode provides a headless interface to Qwen Code that:
- Accepts prompts via command line arguments or stdin
- Returns structured output (text or JSON)
- Supports file redirection and piping
- Enables automation and scripting workflows
- Provides consistent exit codes for error handling
## Basic Usage
### Direct Prompts
Use the `--prompt` (or `-p`) flag to run in headless mode:
```bash
qwen --prompt "What is machine learning?"
```
### Stdin Input
Pipe input to Qwen Code from your terminal:
```bash
echo "Explain this code" | qwen
```
### Combining with File Input
Read from files and process with Qwen Code:
```bash
cat README.md | qwen --prompt "Summarize this documentation"
```
## Output Formats
### Text Output (Default)
Standard human-readable output:
```bash
qwen -p "What is the capital of France?"
```
Response format:
```
The capital of France is Paris.
```
### JSON Output
Returns structured data including response, statistics, and metadata. This
format is ideal for programmatic processing and automation scripts.
#### Response Schema
The JSON output follows this high-level structure:
```json
{
"response": "string", // The main AI-generated content answering your prompt
"stats": {
// Usage metrics and performance data
"models": {
// Per-model API and token usage statistics
"[model-name]": {
"api": {
/* request counts, errors, latency */
},
"tokens": {
/* prompt, response, cached, total counts */
}
}
},
"tools": {
// Tool execution statistics
"totalCalls": "number",
"totalSuccess": "number",
"totalFail": "number",
"totalDurationMs": "number",
"totalDecisions": {
/* accept, reject, modify, auto_accept counts */
},
"byName": {
/* per-tool detailed stats */
}
},
"files": {
// File modification statistics
"totalLinesAdded": "number",
"totalLinesRemoved": "number"
}
},
"error": {
// Present only when an error occurred
"type": "string", // Error type (e.g., "ApiError", "AuthError")
"message": "string", // Human-readable error description
"code": "number" // Optional error code
}
}
```
#### Example Usage
```bash
qwen -p "What is the capital of France?" --output-format json
```
Response:
```json
{
"response": "The capital of France is Paris.",
"stats": {
"models": {
"qwen3-coder-plus": {
"api": {
"totalRequests": 2,
"totalErrors": 0,
"totalLatencyMs": 5053
},
"tokens": {
"prompt": 24939,
"candidates": 20,
"total": 25113,
"cached": 21263,
"thoughts": 154,
"tool": 0
}
}
},
"tools": {
"totalCalls": 1,
"totalSuccess": 1,
"totalFail": 0,
"totalDurationMs": 1881,
"totalDecisions": {
"accept": 0,
"reject": 0,
"modify": 0,
"auto_accept": 1
},
"byName": {
"google_web_search": {
"count": 1,
"success": 1,
"fail": 0,
"durationMs": 1881,
"decisions": {
"accept": 0,
"reject": 0,
"modify": 0,
"auto_accept": 1
}
}
}
},
"files": {
"totalLinesAdded": 0,
"totalLinesRemoved": 0
}
}
}
```
### File Redirection
Save output to files or pipe to other commands:
```bash
# Save to file
qwen -p "Explain Docker" > docker-explanation.txt
qwen -p "Explain Docker" --output-format json > docker-explanation.json
# Append to file
qwen -p "Add more details" >> docker-explanation.txt
# Pipe to other tools
qwen -p "What is Kubernetes?" --output-format json | jq '.response'
qwen -p "Explain microservices" | wc -w
qwen -p "List programming languages" | grep -i "python"
```
## Configuration Options
Key command-line options for headless usage:
| Option | Description | Example |
| ----------------------- | ---------------------------------- | ------------------------------------------------ |
| `--prompt`, `-p` | Run in headless mode | `qwen -p "query"` |
| `--output-format` | Specify output format (text, json) | `qwen -p "query" --output-format json` |
| `--model`, `-m` | Specify the Qwen model | `qwen -p "query" -m qwen3-coder-plus` |
| `--debug`, `-d` | Enable debug mode | `qwen -p "query" --debug` |
| `--all-files`, `-a` | Include all files in context | `qwen -p "query" --all-files` |
| `--include-directories` | Include additional directories | `qwen -p "query" --include-directories src,docs` |
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
| `--approval-mode` | Set approval mode | `qwen -p "query" --approval-mode auto_edit` |
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](./cli/configuration.md).
## Examples
#### Code review
```bash
cat src/auth.py | qwen -p "Review this authentication code for security issues" > security-review.txt
```
#### Generate commit messages
```bash
result=$(git diff --cached | qwen -p "Write a concise commit message for these changes" --output-format json)
echo "$result" | jq -r '.response'
```
#### API documentation
```bash
result=$(cat api/routes.js | qwen -p "Generate OpenAPI spec for these routes" --output-format json)
echo "$result" | jq -r '.response' > openapi.json
```
#### Batch code analysis
```bash
for file in src/*.py; do
echo "Analyzing $file..."
result=$(cat "$file" | qwen -p "Find potential bugs and suggest improvements" --output-format json)
echo "$result" | jq -r '.response' > "reports/$(basename "$file").analysis"
echo "Completed analysis for $(basename "$file")" >> reports/progress.log
done
```
#### Code review
```bash
result=$(git diff origin/main...HEAD | qwen -p "Review these changes for bugs, security issues, and code quality" --output-format json)
echo "$result" | jq -r '.response' > pr-review.json
```
#### Log analysis
```bash
grep "ERROR" /var/log/app.log | tail -20 | qwen -p "Analyze these errors and suggest root cause and fixes" > error-analysis.txt
```
#### Release notes generation
```bash
result=$(git log --oneline v1.0.0..HEAD | qwen -p "Generate release notes from these commits" --output-format json)
response=$(echo "$result" | jq -r '.response')
echo "$response"
echo "$response" >> CHANGELOG.md
```
#### Model and tool usage tracking
```bash
result=$(qwen -p "Explain this database schema" --include-directories db --output-format json)
total_tokens=$(echo "$result" | jq -r '.stats.models // {} | to_entries | map(.value.tokens.total) | add // 0')
models_used=$(echo "$result" | jq -r '.stats.models // {} | keys | join(", ") | if . == "" then "none" else . end')
tool_calls=$(echo "$result" | jq -r '.stats.tools.totalCalls // 0')
tools_used=$(echo "$result" | jq -r '.stats.tools.byName // {} | keys | join(", ") | if . == "" then "none" else . end')
echo "$(date): $total_tokens tokens, $tool_calls tool calls ($tools_used) used with models: $models_used" >> usage.log
echo "$result" | jq -r '.response' > schema-docs.md
echo "Recent usage trends:"
tail -5 usage.log
```
## Resources
- [CLI Configuration](./cli/configuration.md) - Complete configuration guide
- [Authentication](./cli/authentication.md) - Setup authentication
- [Commands](./cli/commands.md) - Interactive commands reference
- [Tutorials](./cli/tutorials.md) - Step-by-step automation guides

View File

@@ -1,344 +0,0 @@
# Welcome to Qwen Code documentation
Qwen Code is a powerful command-line AI workflow tool adapted from [**Gemini CLI**](https://github.com/google-gemini/gemini-cli) ([details](./README.gemini.md)), specifically optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) models. It enhances your development workflow with advanced code understanding, automated tasks, and intelligent assistance.
## 🚀 Why Choose Qwen Code?
- 🎯 **Free Tier:** Up to 60 requests/min and 2,000 requests/day with your [QwenChat](https://chat.qwen.ai/) account.
- 🧠 **Advanced Model:** Specially optimized for [Qwen3-Coder](https://github.com/QwenLM/Qwen3-Coder) for superior code understanding and assistance.
- 🏆 **Comprehensive Features:** Includes subagents, Plan Mode, TodoWrite, vision model support, and full OpenAI API compatibility—all seamlessly integrated.
- 🔧 **Built-in & Extensible Tools:** Includes file system operations, shell command execution, web fetch/search, and more—all easily extended via the Model Context Protocol (MCP) for custom integrations.
- 💻 **Developer-Centric:** Built for terminal-first workflows—perfect for command-line enthusiasts.
- 🛡️ **Open Source:** Apache 2.0 licensed for maximum freedom and transparency.
## Installation
### Prerequisites
Ensure you have [Node.js version 20](https://nodejs.org/en/download) or higher installed.
```bash
curl -qL https://www.npmjs.com/install.sh | sh
```
### Install from npm
```bash
npm install -g @qwen-code/qwen-code@latest
qwen --version
```
### Install from source
```bash
git clone https://github.com/QwenLM/qwen-code.git
cd qwen-code
npm install
npm install -g .
```
### Install globally with Homebrew (macOS/Linux)
```bash
brew install qwen-code
```
## Quick Start
```bash
# Start Qwen Code
qwen
# Example commands
> Explain this codebase structure
> Help me refactor this function
> Generate unit tests for this module
```
### Session Management
Control your token usage with configurable session limits to optimize costs and performance.
#### Configure Session Token Limit
Create or edit `.qwen/settings.json` in your home directory:
```json
{
"sessionTokenLimit": 32000
}
```
#### Session Commands
- **`/compress`** - Compress conversation history to continue within token limits
- **`/clear`** - Clear all conversation history and start fresh
- **`/stats`** - Check current token usage and limits
> 📝 **Note**: Session token limit applies to a single conversation, not cumulative API calls.
### Vision Model Configuration
Qwen Code includes intelligent vision model auto-switching that detects images in your input and can automatically switch to vision-capable models for multimodal analysis. **This feature is enabled by default** - when you include images in your queries, you'll see a dialog asking how you'd like to handle the vision model switch.
#### Skip the Switch Dialog (Optional)
If you don't want to see the interactive dialog each time, configure the default behavior in your `.qwen/settings.json`:
```json
{
"experimental": {
"vlmSwitchMode": "once"
}
}
```
**Available modes:**
- **`"once"`** - Switch to vision model for this query only, then revert
- **`"session"`** - Switch to vision model for the entire session
- **`"persist"`** - Continue with current model (no switching)
- **Not set** - Show interactive dialog each time (default)
#### Command Line Override
You can also set the behavior via command line:
```bash
# Switch once per query
qwen --vlm-switch-mode once
# Switch for entire session
qwen --vlm-switch-mode session
# Never switch automatically
qwen --vlm-switch-mode persist
```
#### Disable Vision Models (Optional)
To completely disable vision model support, add to your `.qwen/settings.json`:
```json
{
"experimental": {
"visionModelPreview": false
}
}
```
> 💡 **Tip**: In YOLO mode (`--yolo`), vision switching happens automatically without prompts when images are detected.
### Authorization
Choose your preferred authentication method based on your needs:
#### 1. Qwen OAuth (🚀 Recommended - Start in 30 seconds)
The easiest way to get started - completely free with generous quotas:
```bash
# Just run this command and follow the browser authentication
qwen
```
**What happens:**
1. **Instant Setup**: CLI opens your browser automatically
2. **One-Click Login**: Authenticate with your qwen.ai account
3. **Automatic Management**: Credentials cached locally for future use
4. **No Configuration**: Zero setup required - just start coding!
**Free Tier Benefits:**
-**2,000 requests/day** (no token counting needed)
-**60 requests/minute** rate limit
-**Automatic credential refresh**
-**Zero cost** for individual users
- **Note**: Model fallback may occur to maintain service quality
#### 2. OpenAI-Compatible API
Use API keys for OpenAI or other compatible providers:
**Configuration Methods:**
1. **Environment Variables**
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="your_api_endpoint"
export OPENAI_MODEL="your_model_choice"
```
2. **Project `.env` File**
Create a `.env` file in your project root:
```env
OPENAI_API_KEY=your_api_key_here
OPENAI_BASE_URL=your_api_endpoint
OPENAI_MODEL=your_model_choice
```
**API Provider Options**
> ⚠️ **Regional Notice:**
>
> - **Mainland China**: Use Alibaba Cloud Bailian or ModelScope
> - **International**: Use Alibaba Cloud ModelStudio or OpenRouter
<details>
<summary><b>🇨🇳 For Users in Mainland China</b></summary>
**Option 1: Alibaba Cloud Bailian** ([Apply for API Key](https://bailian.console.aliyun.com/))
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3-coder-plus"
```
**Option 2: ModelScope (Free Tier)** ([Apply for API Key](https://modelscope.cn/docs/model-service/API-Inference/intro))
- ✅ **2,000 free API calls per day**
- ⚠️ Connect your Aliyun account to avoid authentication errors
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://api-inference.modelscope.cn/v1"
export OPENAI_MODEL="Qwen/Qwen3-Coder-480B-A35B-Instruct"
```
</details>
<details>
<summary><b>🌍 For International Users</b></summary>
**Option 1: Alibaba Cloud ModelStudio** ([Apply for API Key](https://modelstudio.console.alibabacloud.com/))
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
export OPENAI_MODEL="qwen3-coder-plus"
```
**Option 2: OpenRouter (Free Tier Available)** ([Apply for API Key](https://openrouter.ai/))
```bash
export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://openrouter.ai/api/v1"
export OPENAI_MODEL="qwen/qwen3-coder:free"
```
</details>
## Usage Examples
### 🔍 Explore Codebases
```bash
cd your-project/
qwen
# Architecture analysis
> Describe the main pieces of this system's architecture
> What are the key dependencies and how do they interact?
> Find all API endpoints and their authentication methods
```
### 💻 Code Development
```bash
# Refactoring
> Refactor this function to improve readability and performance
> Convert this class to use dependency injection
> Split this large module into smaller, focused components
# Code generation
> Create a REST API endpoint for user management
> Generate unit tests for the authentication module
> Add error handling to all database operations
```
### 🔄 Automate Workflows
```bash
# Git automation
> Analyze git commits from the last 7 days, grouped by feature
> Create a changelog from recent commits
> Find all TODO comments and create GitHub issues
# File operations
> Convert all images in this directory to PNG format
> Rename all test files to follow the *.test.ts pattern
> Find and remove all console.log statements
```
### 🐛 Debugging & Analysis
```bash
# Performance analysis
> Identify performance bottlenecks in this React component
> Find all N+1 query problems in the codebase
# Security audit
> Check for potential SQL injection vulnerabilities
> Find all hardcoded credentials or API keys
```
## Popular Tasks
### 📚 Understand New Codebases
```text
> What are the core business logic components?
> What security mechanisms are in place?
> How does the data flow through the system?
> What are the main design patterns used?
> Generate a dependency graph for this module
```
### 🔨 Code Refactoring & Optimization
```text
> What parts of this module can be optimized?
> Help me refactor this class to follow SOLID principles
> Add proper error handling and logging
> Convert callbacks to async/await pattern
> Implement caching for expensive operations
```
### 📝 Documentation & Testing
```text
> Generate comprehensive JSDoc comments for all public APIs
> Write unit tests with edge cases for this component
> Create API documentation in OpenAPI format
> Add inline comments explaining complex algorithms
> Generate a README for this module
```
### 🚀 Development Acceleration
```text
> Set up a new Express server with authentication
> Create a React component with TypeScript and tests
> Implement a rate limiter middleware
> Add database migrations for new schema
> Configure CI/CD pipeline for this project
```
## Commands & Shortcuts
### Session Commands
- `/help` - Display available commands
- `/clear` - Clear conversation history
- `/compress` - Compress history to save tokens
- `/stats` - Show current session information
- `/exit` or `/quit` - Exit Qwen Code
### Keyboard Shortcuts
- `Ctrl+C` - Cancel current operation
- `Ctrl+D` - Exit (on empty line)
- `Up/Down` - Navigate command history

View File

@@ -1,68 +0,0 @@
[
{
"label": "Overview",
"items": [
{ "label": "Welcome", "slug": "docs" },
{ "label": "Execution and Deployment", "slug": "docs/deployment" },
{ "label": "Architecture Overview", "slug": "docs/architecture" }
]
},
{
"label": "CLI",
"items": [
{ "label": "Introduction", "slug": "docs/cli" },
{ "label": "Authentication", "slug": "docs/cli/authentication" },
{ "label": "Commands", "slug": "docs/cli/commands" },
{ "label": "Configuration", "slug": "docs/cli/configuration" },
{ "label": "Checkpointing", "slug": "docs/checkpointing" },
{ "label": "Extensions", "slug": "docs/extension" },
{ "label": "Headless Mode", "slug": "docs/headless" },
{ "label": "IDE Integration", "slug": "docs/ide-integration" },
{
"label": "IDE Companion Spec",
"slug": "docs/ide-companion-spec"
},
{ "label": "Telemetry", "slug": "docs/telemetry" },
{ "label": "Themes", "slug": "docs/cli/themes" },
{ "label": "Token Caching", "slug": "docs/cli/token-caching" },
{ "label": "Trusted Folders", "slug": "docs/trusted-folders" },
{ "label": "Tutorials", "slug": "docs/cli/tutorials" }
]
},
{
"label": "Core",
"items": [
{ "label": "Introduction", "slug": "docs/core" },
{ "label": "Tools API", "slug": "docs/core/tools-api" },
{ "label": "Memory Import Processor", "slug": "docs/core/memport" }
]
},
{
"label": "Tools",
"items": [
{ "label": "Overview", "slug": "docs/tools" },
{ "label": "File System", "slug": "docs/tools/file-system" },
{ "label": "Multi-File Read", "slug": "docs/tools/multi-file" },
{ "label": "Shell", "slug": "docs/tools/shell" },
{ "label": "Web Fetch", "slug": "docs/tools/web-fetch" },
{ "label": "Web Search", "slug": "docs/tools/web-search" },
{ "label": "Memory", "slug": "docs/tools/memory" },
{ "label": "MCP Servers", "slug": "docs/tools/mcp-server" },
{ "label": "Sandboxing", "slug": "docs/sandbox" }
]
},
{
"label": "Development",
"items": [
{ "label": "NPM", "slug": "docs/npm" },
{ "label": "Releases", "slug": "docs/releases" }
]
},
{
"label": "Support",
"items": [
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
{ "label": "Terms of Service", "slug": "docs/tos-privacy" }
]
}
]

28
docs/users/_meta.ts Normal file
View File

@@ -0,0 +1,28 @@
export default {
'Getting started': {
type: 'separator',
title: 'Getting started', // Title is optional
},
overview: 'Overview',
'quick-start': 'QuickStart',
'common-workflow': 'Command Workflows',
'Outside of the terminal': {
type: 'separator',
title: 'Outside of the terminal', // Title is optional
},
'integration-github-action': 'Github Action',
'integration-vscode': 'VSCode Extension',
'integration-zed': 'Zed IDE',
'Code with Qwen Code': {
type: 'separator',
title: 'Code with Qwen Code', // Title is optional
},
features: 'Features',
configuration: 'Configuration',
reference: 'Reference',
support: 'Support',
// need refine
'ide-integration': {
display: 'hidden',
},
};

View File

View File

@@ -0,0 +1,7 @@
export default {
settings: 'Settings File',
memory: 'Memory Management',
'trusted-folders': 'Trusted Folders',
'qwen-ignore': 'Ignoring Files',
themes: 'Themes',
};

View File

View File

View File

@@ -0,0 +1,15 @@
export default {
subagents: 'Subagents',
'sub-commands': 'Sub Commands',
checkpointing: {
display: 'hidden',
},
headless: 'Headless Mode',
'welcome-back': {
display: 'hidden',
},
'approval-mode': 'Approval Mode',
'token-caching': 'Token Caching',
mcp: 'MCP',
sandbox: 'Sandboxing',
};

View File

View File

@@ -0,0 +1,307 @@
# Headless Mode
Headless mode allows you to run Qwen Code programmatically from command line
scripts and automation tools without any interactive UI. This is ideal for
scripting, automation, CI/CD pipelines, and building AI-powered tools.
- [Headless Mode](#headless-mode)
- [Overview](#overview)
- [Basic Usage](#basic-usage)
- [Direct Prompts](#direct-prompts)
- [Stdin Input](#stdin-input)
- [Combining with File Input](#combining-with-file-input)
- [Output Formats](#output-formats)
- [Text Output (Default)](#text-output-default)
- [JSON Output](#json-output)
- [Example Usage](#example-usage)
- [Stream-JSON Output](#stream-json-output)
- [Input Format](#input-format)
- [File Redirection](#file-redirection)
- [Configuration Options](#configuration-options)
- [Examples](#examples)
- [Code review](#code-review)
- [Generate commit messages](#generate-commit-messages)
- [API documentation](#api-documentation)
- [Batch code analysis](#batch-code-analysis)
- [PR code review](#pr-code-review)
- [Log analysis](#log-analysis)
- [Release notes generation](#release-notes-generation)
- [Model and tool usage tracking](#model-and-tool-usage-tracking)
- [Resources](#resources)
## Overview
The headless mode provides a headless interface to Qwen Code that:
- Accepts prompts via command line arguments or stdin
- Returns structured output (text or JSON)
- Supports file redirection and piping
- Enables automation and scripting workflows
- Provides consistent exit codes for error handling
- Can resume previous sessions scoped to the current project for multi-step automation
## Basic Usage
### Direct Prompts
Use the `--prompt` (or `-p`) flag to run in headless mode:
```bash
qwen --prompt "What is machine learning?"
```
### Stdin Input
Pipe input to Qwen Code from your terminal:
```bash
echo "Explain this code" | qwen
```
### Combining with File Input
Read from files and process with Qwen Code:
```bash
cat README.md | qwen --prompt "Summarize this documentation"
```
### Resume Previous Sessions (Headless)
Reuse conversation context from the current project in headless scripts:
```bash
# Continue the most recent session for this project and run a new prompt
qwen --continue -p "Run the tests again and summarize failures"
# Resume a specific session ID directly (no UI)
qwen --resume 123e4567-e89b-12d3-a456-426614174000 -p "Apply the follow-up refactor"
```
Notes:
- Session data is project-scoped JSONL under `~/.qwen/projects/<sanitized-cwd>/chats`.
- Restores conversation history, tool outputs, and chat-compression checkpoints before sending the new prompt.
## Output Formats
Qwen Code supports multiple output formats for different use cases:
### Text Output (Default)
Standard human-readable output:
```bash
qwen -p "What is the capital of France?"
```
Response format:
```
The capital of France is Paris.
```
### JSON Output
Returns structured data as a JSON array. All messages are buffered and output together when the session completes. This format is ideal for programmatic processing and automation scripts.
The JSON output is an array of message objects. The output includes multiple message types: system messages (session initialization), assistant messages (AI responses), and result messages (execution summary).
#### Example Usage
```bash
qwen -p "What is the capital of France?" --output-format json
```
Output (at end of execution):
```json
[
{
"type": "system",
"subtype": "session_start",
"uuid": "...",
"session_id": "...",
"model": "qwen3-coder-plus",
...
},
{
"type": "assistant",
"uuid": "...",
"session_id": "...",
"message": {
"id": "...",
"type": "message",
"role": "assistant",
"model": "qwen3-coder-plus",
"content": [
{
"type": "text",
"text": "The capital of France is Paris."
}
],
"usage": {...}
},
"parent_tool_use_id": null
},
{
"type": "result",
"subtype": "success",
"uuid": "...",
"session_id": "...",
"is_error": false,
"duration_ms": 1234,
"result": "The capital of France is Paris.",
"usage": {...}
}
]
```
### Stream-JSON Output
Stream-JSON format emits JSON messages immediately as they occur during execution, enabling real-time monitoring. This format uses line-delimited JSON where each message is a complete JSON object on a single line.
```bash
qwen -p "Explain TypeScript" --output-format stream-json
```
Output (streaming as events occur):
```json
{"type":"system","subtype":"session_start","uuid":"...","session_id":"..."}
{"type":"assistant","uuid":"...","session_id":"...","message":{...}}
{"type":"result","subtype":"success","uuid":"...","session_id":"..."}
```
When combined with `--include-partial-messages`, additional stream events are emitted in real-time (message_start, content_block_delta, etc.) for real-time UI updates.
```bash
qwen -p "Write a Python script" --output-format stream-json --include-partial-messages
```
### Input Format
The `--input-format` parameter controls how Qwen Code consumes input from standard input:
- **`text`** (default): Standard text input from stdin or command-line arguments
- **`stream-json`**: JSON message protocol via stdin for bidirectional communication
> **Note:** Stream-json input mode is currently under construction and is intended for SDK integration. It requires `--output-format stream-json` to be set.
### File Redirection
Save output to files or pipe to other commands:
```bash
# Save to file
qwen -p "Explain Docker" > docker-explanation.txt
qwen -p "Explain Docker" --output-format json > docker-explanation.json
# Append to file
qwen -p "Add more details" >> docker-explanation.txt
# Pipe to other tools
qwen -p "What is Kubernetes?" --output-format json | jq '.response'
qwen -p "Explain microservices" | wc -w
qwen -p "List programming languages" | grep -i "python"
# Stream-JSON output for real-time processing
qwen -p "Explain Docker" --output-format stream-json | jq '.type'
qwen -p "Write code" --output-format stream-json --include-partial-messages | jq '.event.type'
```
## Configuration Options
Key command-line options for headless usage:
| Option | Description | Example |
| ---------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------ |
| `--prompt`, `-p` | Run in headless mode | `qwen -p "query"` |
| `--output-format`, `-o` | Specify output format (text, json, stream-json) | `qwen -p "query" --output-format json` |
| `--input-format` | Specify input format (text, stream-json) | `qwen --input-format text --output-format stream-json` |
| `--include-partial-messages` | Include partial messages in stream-json output | `qwen -p "query" --output-format stream-json --include-partial-messages` |
| `--debug`, `-d` | Enable debug mode | `qwen -p "query" --debug` |
| `--all-files`, `-a` | Include all files in context | `qwen -p "query" --all-files` |
| `--include-directories` | Include additional directories | `qwen -p "query" --include-directories src,docs` |
| `--yolo`, `-y` | Auto-approve all actions | `qwen -p "query" --yolo` |
| `--approval-mode` | Set approval mode | `qwen -p "query" --approval-mode auto_edit` |
| `--continue` | Resume the most recent session for this project | `qwen --continue -p "Pick up where we left off"` |
| `--resume [sessionId]` | Resume a specific session (or choose interactively) | `qwen --resume 123e... -p "Finish the refactor"` |
For complete details on all available configuration options, settings files, and environment variables, see the [Configuration Guide](./cli/configuration.md).
## Examples
### Code review
```bash
cat src/auth.py | qwen -p "Review this authentication code for security issues" > security-review.txt
```
### Generate commit messages
```bash
result=$(git diff --cached | qwen -p "Write a concise commit message for these changes" --output-format json)
echo "$result" | jq -r '.response'
```
### API documentation
```bash
result=$(cat api/routes.js | qwen -p "Generate OpenAPI spec for these routes" --output-format json)
echo "$result" | jq -r '.response' > openapi.json
```
### Batch code analysis
```bash
for file in src/*.py; do
echo "Analyzing $file..."
result=$(cat "$file" | qwen -p "Find potential bugs and suggest improvements" --output-format json)
echo "$result" | jq -r '.response' > "reports/$(basename "$file").analysis"
echo "Completed analysis for $(basename "$file")" >> reports/progress.log
done
```
### PR code review
```bash
result=$(git diff origin/main...HEAD | qwen -p "Review these changes for bugs, security issues, and code quality" --output-format json)
echo "$result" | jq -r '.response' > pr-review.json
```
### Log analysis
```bash
grep "ERROR" /var/log/app.log | tail -20 | qwen -p "Analyze these errors and suggest root cause and fixes" > error-analysis.txt
```
### Release notes generation
```bash
result=$(git log --oneline v1.0.0..HEAD | qwen -p "Generate release notes from these commits" --output-format json)
response=$(echo "$result" | jq -r '.response')
echo "$response"
echo "$response" >> CHANGELOG.md
```
### Model and tool usage tracking
```bash
result=$(qwen -p "Explain this database schema" --include-directories db --output-format json)
total_tokens=$(echo "$result" | jq -r '.stats.models // {} | to_entries | map(.value.tokens.total) | add // 0')
models_used=$(echo "$result" | jq -r '.stats.models // {} | keys | join(", ") | if . == "" then "none" else . end')
tool_calls=$(echo "$result" | jq -r '.stats.tools.totalCalls // 0')
tools_used=$(echo "$result" | jq -r '.stats.tools.byName // {} | keys | join(", ") | if . == "" then "none" else . end')
echo "$(date): $total_tokens tokens, $tool_calls tool calls ($tools_used) used with models: $models_used" >> usage.log
echo "$result" | jq -r '.response' > schema-docs.md
echo "Recent usage trends:"
tail -5 usage.log
```
## Resources
- [CLI Configuration](./cli/configuration.md) - Complete configuration guide
- [Authentication](./cli/authentication.md) - Setup authentication
- [Commands](./cli/commands.md) - Interactive commands reference
- [Tutorials](./cli/tutorials.md) - Step-by-step automation guides

View File

View File

View File

@@ -75,9 +75,9 @@ Add to your `.qwen/settings.json`:
### Project Summary Generation
The Welcome Back feature works seamlessly with the `/chat summary` command:
The Welcome Back feature works seamlessly with the `/summary` command:
1. **Generate Summary:** Use `/chat summary` to create a project summary
1. **Generate Summary:** Use `/summary` to create a project summary
2. **Automatic Detection:** Next time you start Qwen Code in this project, Welcome Back will detect the summary
3. **Resume Work:** Choose to continue and the summary will be loaded as context

View File

View File

View File

0
docs/users/overview.md Normal file
View File

View File

View File

@@ -0,0 +1,3 @@
export default {
'keyboard-shortcuts': 'Keyboard Shortcuts',
};

View File

@@ -1,4 +1,6 @@
export default {
troubleshooting: 'Troubleshooting',
'tos-privacy': 'Terms of Service',
Uninstall: 'Uninstall',
};

View File

@@ -0,0 +1,590 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { spawn } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { createInterface } from 'node:readline';
import { setTimeout as delay } from 'node:timers/promises';
import { describe, expect, it } from 'vitest';
import { TestRig } from './test-helper.js';
const REQUEST_TIMEOUT_MS = 60_000;
const INITIAL_PROMPT = 'Create a quick note (smoke test).';
const RESUME_PROMPT = 'Continue the note after reload.';
const LIST_SIZE = 5;
const IS_SANDBOX =
process.env['GEMINI_SANDBOX'] &&
process.env['GEMINI_SANDBOX']!.toLowerCase() !== 'false';
type PendingRequest = {
resolve: (value: unknown) => void;
reject: (reason: Error) => void;
timeout: NodeJS.Timeout;
};
type SessionUpdateNotification = {
sessionId?: string;
update?: {
sessionUpdate?: string;
availableCommands?: Array<{
name: string;
description: string;
input?: { hint: string } | null;
}>;
content?: {
type: string;
text?: string;
};
modeId?: string;
};
};
type PermissionRequest = {
id: number;
sessionId?: string;
toolCall?: {
toolCallId: string;
title: string;
kind: string;
status: string;
content?: Array<{
type: string;
text?: string;
path?: string;
oldText?: string;
newText?: string;
}>;
};
options?: Array<{
optionId: string;
name: string;
kind: string;
}>;
};
type PermissionHandler = (
request: PermissionRequest,
) => { optionId: string } | { outcome: 'cancelled' };
/**
* Sets up an ACP test environment with all necessary utilities.
*/
function setupAcpTest(
rig: TestRig,
options?: { permissionHandler?: PermissionHandler },
) {
const pending = new Map<number, PendingRequest>();
let nextRequestId = 1;
const sessionUpdates: SessionUpdateNotification[] = [];
const permissionRequests: PermissionRequest[] = [];
const stderr: string[] = [];
// Default permission handler: auto-approve all
const permissionHandler =
options?.permissionHandler ?? (() => ({ optionId: 'proceed_once' }));
const agent = spawn('node', [rig.bundlePath, '--experimental-acp'], {
cwd: rig.testDir!,
stdio: ['pipe', 'pipe', 'pipe'],
});
agent.stderr?.on('data', (chunk) => {
stderr.push(chunk.toString());
});
const rl = createInterface({ input: agent.stdout });
const send = (json: unknown) => {
agent.stdin.write(`${JSON.stringify(json)}\n`);
};
const sendResponse = (id: number, result: unknown) => {
send({ jsonrpc: '2.0', id, result });
};
const sendRequest = (method: string, params?: unknown) =>
new Promise<unknown>((resolve, reject) => {
const id = nextRequestId++;
const timeout = setTimeout(() => {
pending.delete(id);
reject(new Error(`Request ${id} (${method}) timed out`));
}, REQUEST_TIMEOUT_MS);
pending.set(id, { resolve, reject, timeout });
send({ jsonrpc: '2.0', id, method, params });
});
const handleResponse = (msg: {
id: number;
result?: unknown;
error?: { message?: string };
}) => {
const waiter = pending.get(msg.id);
if (!waiter) {
return;
}
clearTimeout(waiter.timeout);
pending.delete(msg.id);
if (msg.error) {
waiter.reject(new Error(msg.error.message ?? 'Unknown error'));
} else {
waiter.resolve(msg.result);
}
};
const handleMessage = (msg: {
id?: number;
method?: string;
params?: SessionUpdateNotification & {
path?: string;
content?: string;
sessionId?: string;
toolCall?: PermissionRequest['toolCall'];
options?: PermissionRequest['options'];
};
result?: unknown;
error?: { message?: string };
}) => {
if (typeof msg.id !== 'undefined' && ('result' in msg || 'error' in msg)) {
handleResponse(
msg as {
id: number;
result?: unknown;
error?: { message?: string };
},
);
return;
}
if (msg.method === 'session/update') {
sessionUpdates.push({
sessionId: msg.params?.sessionId,
update: msg.params?.update,
});
return;
}
if (
msg.method === 'session/request_permission' &&
typeof msg.id === 'number'
) {
// Track permission request
const permRequest: PermissionRequest = {
id: msg.id,
sessionId: msg.params?.sessionId,
toolCall: msg.params?.toolCall,
options: msg.params?.options,
};
permissionRequests.push(permRequest);
// Use custom handler or default
const response = permissionHandler(permRequest);
if ('outcome' in response) {
sendResponse(msg.id, { outcome: response });
} else {
sendResponse(msg.id, {
outcome: { optionId: response.optionId, outcome: 'selected' },
});
}
return;
}
if (msg.method === 'fs/read_text_file' && typeof msg.id === 'number') {
try {
const content = readFileSync(msg.params?.path ?? '', 'utf8');
sendResponse(msg.id, { content });
} catch (e) {
sendResponse(msg.id, { content: `ERROR: ${(e as Error).message}` });
}
return;
}
if (msg.method === 'fs/write_text_file' && typeof msg.id === 'number') {
try {
writeFileSync(
msg.params?.path ?? '',
msg.params?.content ?? '',
'utf8',
);
sendResponse(msg.id, null);
} catch (e) {
sendResponse(msg.id, { message: (e as Error).message });
}
}
};
rl.on('line', (line) => {
if (!line.trim()) return;
try {
const msg = JSON.parse(line);
handleMessage(msg);
} catch {
// Ignore non-JSON output from the agent.
}
});
const waitForExit = () =>
new Promise<void>((resolve) => {
if (agent.exitCode !== null || agent.signalCode) {
resolve();
return;
}
agent.once('exit', () => resolve());
});
const cleanup = async () => {
rl.close();
agent.kill();
pending.forEach(({ timeout }) => clearTimeout(timeout));
pending.clear();
await waitForExit();
};
return {
sendRequest,
sendResponse,
cleanup,
stderr,
sessionUpdates,
permissionRequests,
};
}
(IS_SANDBOX ? describe.skip : describe)('acp integration', () => {
it('creates, lists, loads, and resumes a session', async () => {
const rig = new TestRig();
rig.setup('acp load session');
const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig);
try {
const initResult = await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
expect(initResult).toBeDefined();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((initResult as any).agentInfo.version).toBeDefined();
await sendRequest('authenticate', { methodId: 'openai' });
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [{ type: 'text', text: INITIAL_PROMPT }],
});
expect(promptResult).toBeDefined();
await delay(500);
const listResult = (await sendRequest('session/list', {
cwd: rig.testDir!,
size: LIST_SIZE,
})) as { items?: Array<{ sessionId: string }> };
expect(Array.isArray(listResult.items)).toBe(true);
expect(listResult.items?.length ?? 0).toBeGreaterThan(0);
const sessionToLoad = listResult.items![0].sessionId;
await sendRequest('session/load', {
cwd: rig.testDir!,
sessionId: sessionToLoad,
mcpServers: [],
});
const resumeResult = await sendRequest('session/prompt', {
sessionId: sessionToLoad,
prompt: [{ type: 'text', text: RESUME_PROMPT }],
});
expect(resumeResult).toBeDefined();
const sessionsWithUpdates = sessionUpdates
.map((update) => update.sessionId)
.filter(Boolean);
expect(sessionsWithUpdates).toContain(sessionToLoad);
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('returns modes on initialize and allows setting approval mode', async () => {
const rig = new TestRig();
rig.setup('acp approval mode');
const { sendRequest, cleanup, stderr } = setupAcpTest(rig);
try {
// Test 1: Initialize and verify modes are returned
const initResult = (await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
})) as {
protocolVersion: number;
modes: {
currentModeId: string;
availableModes: Array<{
id: string;
name: string;
description: string;
}>;
};
};
expect(initResult).toBeDefined();
expect(initResult.protocolVersion).toBe(1);
// Verify modes data is present
expect(initResult.modes).toBeDefined();
expect(initResult.modes.currentModeId).toBeDefined();
expect(Array.isArray(initResult.modes.availableModes)).toBe(true);
expect(initResult.modes.availableModes.length).toBeGreaterThan(0);
// Verify available modes have expected structure
const modeIds = initResult.modes.availableModes.map((m) => m.id);
expect(modeIds).toContain('default');
expect(modeIds).toContain('yolo');
expect(modeIds).toContain('auto-edit');
expect(modeIds).toContain('plan');
// Verify each mode has required fields
for (const mode of initResult.modes.availableModes) {
expect(mode.id).toBeTruthy();
expect(mode.name).toBeTruthy();
expect(mode.description).toBeTruthy();
}
// Test 2: Authenticate
await sendRequest('authenticate', { methodId: 'openai' });
// Test 3: Create a new session
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Test 4: Set approval mode to 'yolo'
const setModeResult = (await sendRequest('session/set_mode', {
sessionId: newSession.sessionId,
modeId: 'yolo',
})) as { modeId: string };
expect(setModeResult).toBeDefined();
expect(setModeResult.modeId).toBe('yolo');
// Test 5: Set approval mode to 'auto-edit'
const setModeResult2 = (await sendRequest('session/set_mode', {
sessionId: newSession.sessionId,
modeId: 'auto-edit',
})) as { modeId: string };
expect(setModeResult2).toBeDefined();
expect(setModeResult2.modeId).toBe('auto-edit');
// Test 6: Set approval mode back to 'default'
const setModeResult3 = (await sendRequest('session/set_mode', {
sessionId: newSession.sessionId,
modeId: 'default',
})) as { modeId: string };
expect(setModeResult3).toBeDefined();
expect(setModeResult3.modeId).toBe('default');
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('receives available_commands_update with slash commands after session creation', async () => {
const rig = new TestRig();
rig.setup('acp slash commands');
const { sendRequest, cleanup, stderr, sessionUpdates } = setupAcpTest(rig);
try {
// Initialize
await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
await sendRequest('authenticate', { methodId: 'openai' });
// Create a new session
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Wait for available_commands_update to be received
await delay(1000);
// Verify available_commands_update is received
const commandsUpdate = sessionUpdates.find(
(update) =>
update.update?.sessionUpdate === 'available_commands_update',
);
expect(commandsUpdate).toBeDefined();
expect(commandsUpdate?.update?.availableCommands).toBeDefined();
expect(Array.isArray(commandsUpdate?.update?.availableCommands)).toBe(
true,
);
// Verify that the 'init' command is present (the only allowed built-in command for ACP)
const initCommand = commandsUpdate?.update?.availableCommands?.find(
(cmd) => cmd.name === 'init',
);
expect(initCommand).toBeDefined();
expect(initCommand?.description).toBeTruthy();
// Note: We don't test /init execution here because it triggers a complex
// multi-step process (listing files, reading up to 10 files, generating QWEN.md)
// that can take 30-60+ seconds, exceeding the request timeout.
// The slash command execution path is tested via simpler prompts in other tests.
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
it('handles exit plan mode with permission request and mode update notification', async () => {
const rig = new TestRig();
rig.setup('acp exit plan mode');
// Track which permission requests we've seen
const planModeRequests: PermissionRequest[] = [];
const { sendRequest, cleanup, stderr, sessionUpdates, permissionRequests } =
setupAcpTest(rig, {
permissionHandler: (request) => {
// Track all permission requests for later verification
// Auto-approve exit plan mode requests with "proceed_always" to trigger auto-edit mode
if (request.toolCall?.kind === 'switch_mode') {
planModeRequests.push(request);
// Return proceed_always to switch to auto-edit mode
return { optionId: 'proceed_always' };
}
// Auto-approve all other requests
return { optionId: 'proceed_once' };
},
});
try {
// Initialize
await sendRequest('initialize', {
protocolVersion: 1,
clientCapabilities: {
fs: { readTextFile: true, writeTextFile: true },
},
});
await sendRequest('authenticate', { methodId: 'openai' });
// Create a new session
const newSession = (await sendRequest('session/new', {
cwd: rig.testDir!,
mcpServers: [],
})) as { sessionId: string };
expect(newSession.sessionId).toBeTruthy();
// Set mode to 'plan' to enable plan mode
const setModeResult = (await sendRequest('session/set_mode', {
sessionId: newSession.sessionId,
modeId: 'plan',
})) as { modeId: string };
expect(setModeResult.modeId).toBe('plan');
// Send a prompt that should trigger the LLM to call exit_plan_mode
// The prompt is designed to trigger planning behavior
const promptResult = await sendRequest('session/prompt', {
sessionId: newSession.sessionId,
prompt: [
{
type: 'text',
text: 'Create a simple hello world function in Python. Make a brief plan and when ready, use the exit_plan_mode tool to present it for approval.',
},
],
});
expect(promptResult).toBeDefined();
// Give time for all notifications to be processed
await delay(1000);
// Verify: If exit_plan_mode was called, we should have received:
// 1. A permission request with kind: "switch_mode"
// 2. A current_mode_update notification after approval
// Check for switch_mode permission requests
const switchModeRequests = permissionRequests.filter(
(req) => req.toolCall?.kind === 'switch_mode',
);
// Check for current_mode_update notifications
const modeUpdateNotifications = sessionUpdates.filter(
(update) => update.update?.sessionUpdate === 'current_mode_update',
);
// If the LLM called exit_plan_mode, verify the flow
if (switchModeRequests.length > 0) {
// Verify permission request structure
const permReq = switchModeRequests[0];
expect(permReq.toolCall).toBeDefined();
expect(permReq.toolCall?.kind).toBe('switch_mode');
expect(permReq.toolCall?.status).toBe('pending');
expect(permReq.options).toBeDefined();
expect(Array.isArray(permReq.options)).toBe(true);
// Verify options include appropriate choices
const optionKinds = permReq.options?.map((opt) => opt.kind) ?? [];
expect(optionKinds).toContain('allow_once');
expect(optionKinds).toContain('allow_always');
// After approval, should have received current_mode_update
expect(modeUpdateNotifications.length).toBeGreaterThan(0);
// Verify mode update structure
const modeUpdate = modeUpdateNotifications[0];
expect(modeUpdate.sessionId).toBe(newSession.sessionId);
expect(modeUpdate.update?.modeId).toBeDefined();
// Mode should be auto-edit since we approved with proceed_always
expect(modeUpdate.update?.modeId).toBe('auto-edit');
}
// Note: If the LLM didn't call exit_plan_mode, that's acceptable
// since LLM behavior is non-deterministic. The test setup and structure
// is verified regardless.
} catch (e) {
if (stderr.length) {
console.error('Agent stderr:', stderr.join(''));
}
throw e;
} finally {
await cleanup();
}
});
});

View File

@@ -6,124 +6,71 @@
import { describe, it, expect } from 'vitest';
import { TestRig } from './test-helper.js';
import * as fs from 'node:fs';
import * as path from 'node:path';
describe('Ctrl+C exit', () => {
// (#9782) Temporarily disabling on windows because it is failing on main and every
// PR, which is potentially hiding other failures
it.skipIf(process.platform === 'win32')(
'should exit gracefully on second Ctrl+C',
async () => {
const rig = new TestRig();
await rig.setup('should exit gracefully on second Ctrl+C');
it.skip('should exit gracefully on second Ctrl+C', async () => {
const rig = new TestRig();
await rig.setup('should exit gracefully on second Ctrl+C');
const { ptyProcess, promise } = rig.runInteractive();
const { ptyProcess, promise } = rig.runInteractive();
let output = '';
ptyProcess.onData((data) => {
output += data;
});
let output = '';
ptyProcess.onData((data) => {
output += data;
});
// Wait for the app to be ready by looking for the initial prompt indicator
await rig.poll(() => output.includes('▶'), 5000, 100);
const isReady = await rig.waitForText('Type your message', 15000);
expect(isReady, 'CLI did not start up in interactive mode correctly').toBe(
true,
);
// Send first Ctrl+C
ptyProcess.write(String.fromCharCode(3));
// Send first Ctrl+C
ptyProcess.write(String.fromCharCode(3));
// Wait for the exit prompt
await rig.poll(
() => output.includes('Press Ctrl+C again to exit'),
1500,
50,
);
// Wait for the exit prompt
const showedExitPrompt = await rig.poll(
() => output.includes('Press Ctrl+C again to exit'),
1500,
50,
);
expect(showedExitPrompt, `Exit prompt not shown. Output: ${output}`).toBe(
true,
);
// Send second Ctrl+C
ptyProcess.write(String.fromCharCode(3));
// Send second Ctrl+C
ptyProcess.write(String.fromCharCode(3));
const result = await promise;
// Wait for process exit with timeout to fail fast
const EXIT_TIMEOUT = 5000;
const result = await Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(
new Error(
`Process did not exit within ${EXIT_TIMEOUT}ms. Output: ${output}`,
),
),
EXIT_TIMEOUT,
),
),
]);
// Expect a graceful exit (code 0)
expect(
result.exitCode,
`Process exited with code ${result.exitCode}. Output: ${result.output}`,
).toBe(0);
// Expect a graceful exit (code 0)
expect(
result.exitCode,
`Process exited with code ${result.exitCode}. Output: ${result.output}`,
).toBe(0);
// Check that the quitting message is displayed
const quittingMessage = 'Agent powering down. Goodbye!';
// The regex below is intentionally matching the ESC control character (\x1b)
// to strip ANSI color codes from the terminal output.
// eslint-disable-next-line no-control-regex
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
expect(cleanOutput).toContain(quittingMessage);
},
);
it.skipIf(process.platform === 'win32')(
'should exit gracefully on second Ctrl+C when calling a tool',
async () => {
const rig = new TestRig();
await rig.setup(
'should exit gracefully on second Ctrl+C when calling a tool',
);
const childProcessFile = 'child_process_file.txt';
rig.createFile(
'wait.js',
`setTimeout(() => require('fs').writeFileSync('${childProcessFile}', 'done'), 5000)`,
);
const { ptyProcess, promise } = rig.runInteractive();
let output = '';
ptyProcess.onData((data) => {
output += data;
});
// Wait for the app to be ready by looking for the initial prompt indicator
await rig.poll(() => output.includes('▶'), 5000, 100);
ptyProcess.write('use the tool to run "node -e wait.js"\n');
await rig.poll(() => output.includes('Shell'), 5000, 100);
// Send first Ctrl+C
ptyProcess.write(String.fromCharCode(3));
// Wait for the exit prompt
await rig.poll(
() => output.includes('Press Ctrl+C again to exit'),
1500,
50,
);
// Send second Ctrl+C
ptyProcess.write(String.fromCharCode(3));
const result = await promise;
// Expect a graceful exit (code 0)
expect(
result.exitCode,
`Process exited with code ${result.exitCode}. Output: ${result.output}`,
).toBe(0);
// Check that the quitting message is displayed
const quittingMessage = 'Agent powering down. Goodbye!';
// The regex below is intentionally matching the ESC control character (\x1b)
// to strip ANSI color codes from the terminal output.
// eslint-disable-next-line no-control-regex
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
expect(cleanOutput).toContain(quittingMessage);
// Check that the child process was terminated and did not create the file.
const childProcessFileExists = fs.existsSync(
path.join(rig.testDir!, childProcessFile),
);
expect(
childProcessFileExists,
'Child process file should not exist',
).toBe(false);
},
);
// Check that the quitting message is displayed
const quittingMessage = 'Agent powering down. Goodbye!';
// The regex below is intentionally matching the ESC control character (\x1b)
// to strip ANSI color codes from the terminal output.
// eslint-disable-next-line no-control-regex
const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, '');
expect(cleanOutput).toContain(quittingMessage);
});
});

View File

@@ -19,7 +19,7 @@ describe('JSON output', () => {
await rig.cleanup();
});
it('should return a valid JSON with response and stats', async () => {
it('should return a valid JSON array with result message containing response and stats', async () => {
const result = await rig.run(
'What is the capital of France?',
'--output-format',
@@ -27,15 +27,213 @@ describe('JSON output', () => {
);
const parsed = JSON.parse(result);
expect(parsed).toHaveProperty('response');
expect(typeof parsed.response).toBe('string');
expect(parsed.response.toLowerCase()).toContain('paris');
// The output should be an array of messages
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBeGreaterThan(0);
expect(parsed).toHaveProperty('stats');
expect(typeof parsed.stats).toBe('object');
// Find the result message (should be the last message)
const resultMessage = parsed.find(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'result',
);
expect(resultMessage).toBeDefined();
expect(resultMessage).toHaveProperty('is_error');
expect(resultMessage.is_error).toBe(false);
expect(resultMessage).toHaveProperty('result');
expect(typeof resultMessage.result).toBe('string');
expect(resultMessage.result.toLowerCase()).toContain('paris');
// Stats may be present if available
if ('stats' in resultMessage) {
expect(typeof resultMessage.stats).toBe('object');
}
});
it('should return line-delimited JSON messages for stream-json output format', async () => {
const result = await rig.run(
'What is the capital of France?',
'--output-format',
'stream-json',
);
// Stream-json output is line-delimited JSON (one JSON object per line)
const lines = result
.trim()
.split('\n')
.filter((line) => line.trim());
expect(lines.length).toBeGreaterThan(0);
// Parse each line as a JSON object
const messages: unknown[] = [];
for (const line of lines) {
try {
const parsed = JSON.parse(line);
messages.push(parsed);
} catch (parseError) {
throw new Error(
`Failed to parse JSON line: ${line}. Error: ${parseError}`,
);
}
}
// Should have at least system, assistant, and result messages
expect(messages.length).toBeGreaterThanOrEqual(3);
// Find system message
const systemMessage = messages.find(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'system',
);
expect(systemMessage).toBeDefined();
expect(systemMessage).toHaveProperty('subtype');
expect(systemMessage).toHaveProperty('session_id');
// Find assistant message
const assistantMessage = messages.find(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'assistant',
);
expect(assistantMessage).toBeDefined();
expect(assistantMessage).toHaveProperty('message');
expect(assistantMessage).toHaveProperty('session_id');
// Find result message (should be the last message)
const resultMessage = messages[messages.length - 1] as {
type: string;
is_error: boolean;
result: string;
};
expect(resultMessage).toBeDefined();
expect(
typeof resultMessage === 'object' &&
resultMessage !== null &&
'type' in resultMessage &&
resultMessage.type === 'result',
).toBe(true);
expect(resultMessage).toHaveProperty('is_error');
expect(resultMessage.is_error).toBe(false);
expect(resultMessage).toHaveProperty('result');
expect(typeof resultMessage.result).toBe('string');
expect(resultMessage.result.toLowerCase()).toContain('paris');
});
it('should include stream events when using stream-json with include-partial-messages', async () => {
const result = await rig.run(
'What is the capital of France?',
'--output-format',
'stream-json',
'--include-partial-messages',
);
// Stream-json output is line-delimited JSON (one JSON object per line)
const lines = result
.trim()
.split('\n')
.filter((line) => line.trim());
expect(lines.length).toBeGreaterThan(0);
// Parse each line as a JSON object
const messages: unknown[] = [];
for (const line of lines) {
try {
const parsed = JSON.parse(line);
messages.push(parsed);
} catch (parseError) {
throw new Error(
`Failed to parse JSON line: ${line}. Error: ${parseError}`,
);
}
}
// Should have more messages than without include-partial-messages
// because we're including stream events
expect(messages.length).toBeGreaterThan(3);
// Find stream_event messages
const streamEvents = messages.filter(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'stream_event',
);
expect(streamEvents.length).toBeGreaterThan(0);
// Verify stream event structure
const firstStreamEvent = streamEvents[0];
expect(firstStreamEvent).toHaveProperty('event');
expect(firstStreamEvent).toHaveProperty('session_id');
expect(firstStreamEvent).toHaveProperty('uuid');
// Check for expected stream event types
const eventTypes = streamEvents.map((event: unknown) =>
typeof event === 'object' &&
event !== null &&
'event' in event &&
typeof event.event === 'object' &&
event.event !== null &&
'type' in event.event
? event.event.type
: null,
);
// Should have message_start event
expect(eventTypes).toContain('message_start');
// Should have content_block_start event
expect(eventTypes).toContain('content_block_start');
// Should have content_block_delta events
expect(eventTypes).toContain('content_block_delta');
// Should have content_block_stop event
expect(eventTypes).toContain('content_block_stop');
// Should have message_stop event
expect(eventTypes).toContain('message_stop');
// Verify that we still have the complete assistant message
const assistantMessage = messages.find(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'assistant',
);
expect(assistantMessage).toBeDefined();
expect(assistantMessage).toHaveProperty('message');
// Verify that we still have the result message
const resultMessage = messages[messages.length - 1] as {
type: string;
is_error: boolean;
result: string;
};
expect(resultMessage).toBeDefined();
expect(
typeof resultMessage === 'object' &&
resultMessage !== null &&
'type' in resultMessage &&
resultMessage.type === 'result',
).toBe(true);
expect(resultMessage).toHaveProperty('is_error');
expect(resultMessage.is_error).toBe(false);
expect(resultMessage).toHaveProperty('result');
expect(resultMessage.result.toLowerCase()).toContain('paris');
});
it('should return a JSON error for enforced auth mismatch before running', async () => {
const originalOpenaiApiKey = process.env['OPENAI_API_KEY'];
process.env['OPENAI_API_KEY'] = 'test-key';
await rig.setup('json-output-auth-mismatch', {
settings: {
@@ -50,38 +248,63 @@ describe('JSON output', () => {
} catch (e) {
thrown = e as Error;
} finally {
delete process.env['OPENAI_API_KEY'];
process.env['OPENAI_API_KEY'] = originalOpenaiApiKey;
}
expect(thrown).toBeDefined();
const message = (thrown as Error).message;
// Use a regex to find the first complete JSON object in the string
const jsonMatch = message.match(/{[\s\S]*}/);
// Fail if no JSON-like text was found
// The error JSON is written to stdout as a CLIResultMessageError
// Extract stdout from the error message
const stdoutMatch = message.match(/Stdout:\n([\s\S]*?)(?:\n\nStderr:|$)/);
expect(
jsonMatch,
'Expected to find a JSON object in the error output',
stdoutMatch,
'Expected to find stdout in the error message',
).toBeTruthy();
let payload;
const stdout = stdoutMatch![1];
let parsed: unknown[];
try {
// Parse the matched JSON string
payload = JSON.parse(jsonMatch![0]);
// Parse the JSON array from stdout
parsed = JSON.parse(stdout);
} catch (parseError) {
console.error('Failed to parse the following JSON:', jsonMatch![0]);
console.error('Failed to parse the following JSON:', stdout);
throw new Error(
`Test failed: Could not parse JSON from error message. Details: ${parseError}`,
`Test failed: Could not parse JSON from stdout. Details: ${parseError}`,
);
}
expect(payload.error).toBeDefined();
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
// The output should be an array of messages
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBeGreaterThan(0);
// Find the result message with error
const resultMessage = parsed.find(
(msg: unknown) =>
typeof msg === 'object' &&
msg !== null &&
'type' in msg &&
msg.type === 'result' &&
'is_error' in msg &&
msg.is_error === true,
) as {
type: string;
is_error: boolean;
subtype: string;
error?: { message: string; type?: string };
};
expect(resultMessage).toBeDefined();
expect(resultMessage.is_error).toBe(true);
expect(resultMessage).toHaveProperty('subtype');
expect(resultMessage.subtype).toBe('error_during_execution');
expect(resultMessage).toHaveProperty('error');
expect(resultMessage.error).toBeDefined();
expect(resultMessage.error?.message).toContain(
'configured auth type is qwen-oauth',
);
expect(payload.error.message).toContain('current auth type is openai');
expect(resultMessage.error?.message).toContain(
'current auth type is openai',
);
});
});

View File

@@ -340,7 +340,8 @@ export class TestRig {
// as it would corrupt the JSON
const isJsonOutput =
commandArgs.includes('--output-format') &&
commandArgs.includes('json');
(commandArgs.includes('json') ||
commandArgs.includes('stream-json'));
// If we have stderr output and it's not a JSON test, include that also
if (stderr && !isJsonOutput) {
@@ -349,7 +350,23 @@ export class TestRig {
resolve(result);
} else {
reject(new Error(`Process exited with code ${code}:\n${stderr}`));
// Check if this is a JSON output test - for JSON errors, the error is in stdout
const isJsonOutputOnError =
commandArgs.includes('--output-format') &&
(commandArgs.includes('json') ||
commandArgs.includes('stream-json'));
// For JSON output tests, include stdout in the error message
// as the error JSON is written to stdout
if (isJsonOutputOnError && stdout) {
reject(
new Error(
`Process exited with code ${code}:\nStdout:\n${stdout}\n\nStderr:\n${stderr}`,
),
);
} else {
reject(new Error(`Process exited with code ${code}:\n${stderr}`));
}
}
});
});

23
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.2.2",
"version": "0.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.2.2",
"version": "0.4.0",
"workspaces": [
"packages/*"
],
@@ -5255,6 +5255,15 @@
"node": ">= 0.4"
}
},
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -14687,7 +14696,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
@@ -16024,7 +16032,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.2.2",
"version": "0.4.0",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -16139,7 +16147,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.2.2",
"version": "0.4.0",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -16158,6 +16166,7 @@
"@xterm/headless": "5.5.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.0",
"async-mutex": "^0.5.0",
"chardet": "^2.1.0",
"diff": "^7.0.0",
"dotenv": "^17.1.0",
@@ -16278,7 +16287,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.2.2",
"version": "0.4.0",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -16290,7 +16299,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.2.2",
"version": "0.4.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

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