* fix(cli): auto-restore prompt and preserve queue on cancel; align with Claude Code
When a user pressed ESC immediately after submitting a prompt (before the
model produced any meaningful output), qwen-code left the cancelled prompt
stranded in the transcript and in cross-session ↑-history. Cancelling
during tool execution also silently dropped any queued follow-up input.
Mirror Claude Code's auto-restore-on-interrupt:
- Drain the queue back into the input buffer on EVERY cancel path,
including tool-execution cancels (replaces the unconditional
clearQueue() that motivated #3204 with a non-destructive pop).
- When the user cancels with no draft text, no queued input, and no
meaningful pending/committed assistant content, truncate the user
item and trailing INFO from history and pull the prompt text back
into the input box for editing.
- Add Logger.removeLastUserMessage so the disk-backed cross-session
↑-history (getPreviousUserMessages) is also cleaned on cancel.
The "meaningful content" check matches Claude Code's
messagesAfterAreOnlySynthetic: gemini text and tool runs are meaningful;
info/error/warning/retry/notification/tool_use_summary/thoughts are
synthetic. truncateToItem uses functional setState so it batches with
the INFO addItem from cancelOngoingRequest in the same render pass —
no flicker.
Tests cover all five guard branches and the logger undo across normal,
no-op, one-shot, MODEL_SWITCH-interleaved, disk-rotation, and
uninitialized cases.
* fix(core): clear lastLoggedUserEntry on logMessage write failure
Without this, a transient writeFile error during a USER logMessage left
the undo tracker pointing at the previous successful entry. A subsequent
removeLastUserMessage (e.g., from auto-restore on cancel) would then
silently delete an unrelated earlier row from disk-backed history.
Add a regression test that mocks a writeFile rejection and asserts the
tracker is null and the prior entry survives.
Reported in PR review.
* fix(cli, core): share Logger across AppContainer/useGeminiStream and serialize writes
PR-review follow-up addressing two issues in the cancel-undo path.
1. Logger instance mismatch (Critical):
`useGeminiStream` and `AppContainer` each called `useLogger()`, which
instantiates a fresh `Logger` per call. `lastLoggedUserEntry` lives on
the instance, so the undo invoked from `AppContainer` was always a
no-op — the cancelled prompt still surfaced via cross-session
`getPreviousUserMessages`. Move the `useLogger` ownership to
`AppContainer` and pass the same instance into `useGeminiStream` via a
new optional `logger` parameter.
2. Logger write ordering:
Both `logMessage` and `removeLastUserMessage` do read → splice/append
→ writeFile without a lock. A fast cancel-then-resubmit could let
`removeLast` clobber a just-appended new entry. Add a per-instance
`serialize()` helper (a Promise-chained write queue) and route both
mutating ops through it. Reset the queue on `close()`. New regression
test fires removeLast and a fresh logMessage in parallel and asserts
the resubmitted entry survives.
3. Stale React-state race in cancel guard (Suggestion):
The auto-restore guard read `pendingGeminiHistoryItems` from React
state, which can lag a stream chunk that just set
`pendingHistoryItemRef.current`. Snapshot the pending item at the
start of `cancelOngoingRequest` and pass it through the new
`onCancelSubmit({ pendingItem })` info parameter. The guard combines
it with the React-state items so any meaningful in-flight content
blocks auto-restore even before re-render. New test covers the case
where pendingHistoryItems is empty but info.pendingItem carries
`gemini_content`.
All touched-area suites pass: 64 cli AppContainer, 9 historyUtils,
85 useGeminiStream, 46 core logger.
* fix(cli): unbreak build after import-merge regression and tighten cancel-handler test types
The pre-commit eslint --fix on the previous commit collapsed the two
consecutive `import { ... } from '@qwen-code/qwen-code-core'` blocks in
useGeminiStream.ts into a single statement, but kept the `import type`
modifier from the first block — silently turning every runtime symbol
(SendMessageType, MessageSenderType, GitService, ApprovalMode, …) into
type-only imports. tsc rejected with TS2206 + a wave of TS1361 errors
that only surfaced on CI.
Restore the two separate imports: pure-type symbols (Logger included)
in `import type { ... }`, runtime symbols in plain `import { ... }`.
Also: the AppContainer cancel-handler tests captured `onCancelSubmit`
as `() => void`, but the hook signature now takes an optional info
arg. Widen the captured-callback type so passing `{ pendingItem }`
typechecks (TS2554 on line 1053).
* fix(cli, core): tighten cancel-undo robustness from PR review batch 3
Four follow-ups from a /review pass on the auto-restore-on-cancel path.
* logger.ts — only invalidate `lastLoggedUserEntry` when the failed
write was itself a USER attempt. A failed non-USER write (MODEL_SWITCH
on a transient disk error, etc.) doesn't change which row was the
most recent user prompt, so the prior undo target is still valid.
Without this, MODEL_SWITCH disk hiccups silently disabled cancel-undo.
* useGeminiStream.ts — wrap `onCancelSubmit` in try/finally so a throw
in AppContainer's cancel handler can't strand the stream in
Responding (the UI would lock — Esc would no-op until process
restart). `setIsResponding(false)` and `setShellInputFocused(false)`
always run.
* useGeminiStream.ts — also document the three-way coupling between
the INFO `addItem` here and AppContainer's auto-restore guard:
the guard reads `historyRef.current` which doesn't yet contain
this INFO (React batches), and the guard's correctness depends on
the items added here staying synthetic.
* historyUtils.ts — make `isSyntheticHistoryItem` exhaustive over the
35-member `HistoryItemWithoutId` union. Every case is explicit; the
default branch carries a `_exhaustive: never` so adding a new
HistoryItem variant without classifying it triggers a compile-time
error rather than silently disabling auto-restore. Runtime fallback
is "meaningful" (safe — bail rather than wipe content).
Tests: +1 logger case (non-USER failure preserves the USER tracker),
+1 useGeminiStream case (throwing handler still flushes Responding).
All touched suites pass: 47 logger, 9 historyUtils, 86 useGeminiStream,
64 AppContainer.
* docs(core): clarify Logger writeQueue scope (log-history only, not checkpoints)
Reword the comment above `writeQueue` and the `serialize()` JSDoc to
state explicitly that the queue only serializes log-history mutations
(`logMessage` / `removeLastUserMessage`). Checkpoint ops
(saveCheckpoint / deleteCheckpoint / loadCheckpoint) touch separate
files and intentionally don't share this queue, so the previous
"every disk-mutating op chains here" wording overstated the
guarantee.
* fix(cli): flush buffered stream events before snapshotting pendingItem on cancel
Stream content/thought events are throttled into a per-turn `bufferedEvents`
array; only when `flushBufferedStreamEvents` runs do they reach
`pendingHistoryItemRef.current`. Snapshotting BEFORE the flush meant cancels
that fired inside the throttle window (60ms) saw a null `pendingItem` even
when meaningful text was sitting in the buffer. AppContainer's auto-restore
guard then read null, decided "model produced nothing", and called
`truncateToItem` — which silently wiped the very content that the
subsequent `addItem(pendingHistoryItemRef.current)` had just committed.
Move the snapshot to AFTER the flush so it sees the same value as the
addItem call directly below it.
Regression test: yields a content event and cancels without advancing
fake timers, asserts `info.pendingItem` carries the buffered "partial
response" text rather than null.
* fix(core): apply Logger.removeLastUserMessage in-memory removal synchronously
AppContainer's `userMessages` effect calls `getPreviousUserMessages()`
on the same render that history truncation fires (it depends on
`historyManager.history`). The previous implementation only updated
`this.logs` after `await fs.writeFile(...)` settled, so the effect
read stale logs and ↑-history surfaced the cancelled prompt until
some unrelated future history change re-ran the effect.
Move the cache filter ahead of the serialize queue so consumers see
the removal immediately. The async serialize op continues to read,
splice, and write disk, then re-syncs `this.logs` from disk on
success or rotation.
Regression test fires removeLast without awaiting, then asserts the
very next `getPreviousUserMessages()` returns [] (no cancelled
prompt), and that the background promise still resolves to true.
* docs(core, cli): clarify removeLastUserMessage contract; observability for cancel-undo
* logger.ts — extend the JSDoc on `removeLastUserMessage` to spell
out the two-phase semantics (sync optimistic in-memory removal +
async serialized disk reconciliation), and explicitly document that
the boolean return value reflects the *disk* outcome while the
in-memory cache is updated unconditionally. Also explain why disk
failures are NOT rolled back: rolling back would resurrect the
cancelled prompt in ↑-history, which is worse UX than a temporary
cache/disk divergence (which converges on next op or on
`initialize()` of the next session).
* AppContainer.tsx — wrap the fire-and-forget
`logger.removeLastUserMessage()` in `.catch(debugLogger.debug)`.
The Logger's internal try/catches mean the Promise should never
reject today, but a future code-path change shouldn't surface as
an UnhandledPromiseRejection — and a debug-level log is the right
observability hook for "cancel succeeded in UI but disk-undo
failed silently".
* fix(core,cli): #4023 review wave — logger atomicity + observable undo failure
3 #4023 review threads addressed:
- core/logger.ts: `removeLastUserMessage` now ROLLS BACK the
optimistic in-memory removal when the disk read or write fails.
Previously the JSDoc/return contract was violated: the method
returned `false` on failure but `this.logs` already showed the
entry removed — callers (AppContainer's `userMessages` effect)
saw the inconsistency and the cancelled prompt vanished from
↑-history despite the disk still carrying it. The rollback
re-inserts the target at its original index when no concurrent
mutation took its place, and restores `lastLoggedUserEntry` so
a follow-up retry has a target. Regression test pinned: spy on
fs.writeFile to throw, assert `removed === false` AND
getPreviousUserMessages() still surfaces the entry.
- cli/AppContainer.tsx: `void logger?.removeLastUserMessage()` no
longer silently swallows failures. Added `.catch` that routes
through `debugLogger.debug` so a disk-write failure leaves a
diagnostic trail; without it the cancelled prompt would
resurrect next session via ↑-history with no observability into
why.
- cli/historyUtils.ts: `gemini_thought` / `gemini_thought_content`
classification reaffirmed as SYNTHETIC with explicit JSDoc on
WHY (Claude Code parity + auto-restore is most valuable in the
cancel-during-thinking case which is exactly the case where
thoughts have appeared but no committed `gemini_content`).
Future readers won't re-litigate the classification by accident.
Tests: 49/49 logger.test.ts pass; tsc + ESLint clean.
* docs(core): align removeLastUserMessage JSDoc with rollback-on-failure behaviour
The previous commit added a rollback path to `removeLastUserMessage`
(re-insert the optimistically-removed entry and restore
`lastLoggedUserEntry` when the disk read or write throws), but the
JSDoc still said the in-memory removal is "intentionally NOT rolled
back" — a copy-paste leftover from the earlier design that picked
optimistic-and-diverge. Rewrite the failure-handling paragraph and
`@returns` line to describe the rollback contract instead.
No code change.
* fix(cli, core): scope auto-restore to the cancelled turn + tighten typings/tests
Three follow-ups from PR #4023 review batch 5.
* cli — `CancelSubmitInfo` gains `lastTurnUserItem` carrying the user
prompt text that THIS turn's `prepareQueryForGemini` added (or
`null` for paths that don't push a user history item: Cron /
Notification / slash `submit_prompt`). `cancelOngoingRequest`
snapshots `lastTurnUserItemRef.current` and ships it through. The
AppContainer auto-restore guard now requires
`info.lastTurnUserItem` to be present AND match the candidate
user item's text before truncating/rewinding — closing the case
where an older user item happens to be followed by only-synthetic
trailing content and the current cancelled turn never owned a
user item to begin with.
Two new regression tests pin both halves: cancel of a non-USER
turn bails despite trailing-synthetic, and a deliberate text
mismatch also bails.
* cli — `.catch((err)` widened to `(err: unknown)` on the
fire-and-forget `logger.removeLastUserMessage()` call. Belt-and-
braces: `Promise.catch`'s lib typing is `(reason: any) =>` so
this is not currently TS7006, but tightening keeps the codebase
ready for `@typescript-eslint/no-implicit-any-catch`-style rules
and matches the rest of the codebase's strict-error patterns.
* core — Added a `removeLastUserMessage` regression test pinning
the `_readLogFile` failure branch (mocks `fs.readFile` to throw
Permission denied). The symmetric `writeFile` failure case was
already covered; this closes the gap on the read leg.
Tests: AppContainer 67/67 (+2), useGeminiStream 87/87, historyUtils 11/11,
logger 50/50 (+1). Type-check and lint clean.
* chore(cli): add debug observability for each auto-restore-on-cancel bail-out
The cancel handler in AppContainer has seven independent guards that
silently `return` when auto-restore is unsafe (buffer non-empty, queue
non-empty, pending meaningful content, no last-turn user item, no user
in history, trailing items not all synthetic, candidate-text mismatch).
Until now, users reporting "I pressed ESC but my prompt didn't come
back" had no way to know which guard tripped without a debugger.
Log a specific `debugLogger.debug(...)` line at each bail-out and one
on the success path. Debug level keeps production output silent;
re-enableable by running with `DEBUG=1` (per existing convention in
this file). No control-flow change.
* docs(core): scope removeLastUserMessage's "false ⇒ observable in-memory" guarantee
The previous JSDoc implied the guarantee held for every `false` return,
but it only really holds on the disk read/write THROW path (where we
roll back the optimistic in-memory removal). Two other `false`-paths
behave differently:
- Initial guards (logger uninitialized / no tracked entry): nothing
was ever removed, nothing to restore — entry stays in whatever
state it was already in.
- Disk read succeeds but the tracked row is missing on disk (e.g. a
concurrent rotation/clear): we adopt disk state into `this.logs`,
so both sides agree the entry is gone — `false` is returned but
the entry is NOT observable in-memory either.
Rewrite the failure-handling paragraph and `@returns` line to spell
out both branches explicitly. No code change.
* fix(core): shift lastLoggedUserEntry on USER logMessage duplicate-skip
When `_updateLogFile` detects another instance already wrote an
identical (sessionId, messageId, timestamp, message) row and returns
null, the previous logMessage code path left `lastLoggedUserEntry`
pointing at the prior USER entry. A subsequent cancel/auto-restore
would then call `removeLastUserMessage()` and silently delete the
wrong row — typically an older prompt that the user did not intend
to undo.
The fix: when the duplicate skip happens on a USER attempt, advance
`lastLoggedUserEntry` to the entry object we just tried to write.
`_updateLogFile` mutates that object's `messageId` in-place to align
with the disk row before the duplicate check, so the 5-tuple matches
the row that's actually on disk and an undo correctly targets it.
The natural race (`max+1` colliding with an existing `messageId`)
is not reachable by sequential awaits — the snapshot used for the
duplicate check is always max+1-strict. The regression test drives
the contract directly by mocking `_updateLogFile` to resolve to
null and asserting `lastLoggedUserEntry` shifts to the new entry.
* fix(cli, core): strip orphan user entry from chat history on auto-restore
The auto-restore branch was cleaning up two of the three places a
cancelled prompt lives — the UI transcript via `truncateToItem` and
the disk-backed ↑-history via `Logger.removeLastUserMessage`. The
third — the in-memory chat history on `GeminiChat` — was left
untouched. `sendMessageStream` appends the user content to
`chat.history` BEFORE the stream generator runs and the abort path
doesn't pop it. After a successful auto-restore the next request's
wire payload still carried the cancelled prompt as a leading user
turn alongside the new prompt, so the model saw context the user
believed had been undone (and in some shapes the API would reject
two consecutive user turns).
Mirror the existing strip the Retry submit path uses
(`GeminiClient.sendMessageStream` at the `Retry` branch): make
`GeminiClient.stripOrphanedUserEntriesFromHistory` public and call
it from the auto-restore success path, sitting next to the UI
truncate and the disk-log undo. The method already pops trailing
user entries and clears the `FileReadCache` (which can otherwise
hold dangling `read_file` results from the stripped turn).
End-to-end reproduction from the PR review:
1. Submit `what time is it?` → ESC during pre-token delay →
auto-restore (UI rewound, buffer pre-filled).
2. Edit buffer to `what year is it?` → submit.
3. Pre-fix: outbound `messages` carried both prompts as consecutive
user turns. Post-fix: only the new prompt.
Test: extend the auto-restore-success AppContainer test with a
mock `stripOrphanedUserEntriesFromHistory` spy and assert it fires.
The non-restore branches don't install the spy (it's optionally
chained at the call site).
* fix(core, cli): tighten Logger.serialize signature + pin lastTurnUserItem and dup-skip identity contracts
Three follow-ups from PR review batch:
* core/logger.ts — `serialize()` was `this.writeQueue.then(op, op)`.
The second callback was dead code: `writeQueue` is seeded with
`Promise.resolve()` and reassigned through `.catch(() => undefined)`,
so the queue tail can never reject. Worse, `then(op, op)` reads as
"retry op on rejection" — wrong intent. Switch to `.then(() => op())`
with a comment spelling out the no-reject invariant.
* cli/useGeminiStream.test.tsx — add ownership-contract tests at the
PRODUCER side of `info.lastTurnUserItem`. Until now only the
AppContainer tests pinned the contract, and they fabricate the
value, so a regression that drops `lastTurnUserItemRef.current = {
text: trimmedQuery }` in `prepareQueryForGemini` would slip
through. New tests:
- normal `UserQuery` submit → cancel → assert
`info.lastTurnUserItem === { text: 'what time is it?' }`.
- `SendMessageType.Notification` submit → cancel → assert
`info.lastTurnUserItem === null` (path doesn't push a user
history item, the ref reset at the top of
prepareQueryForGemini must keep it null).
* core/logger.test.ts — strengthen the duplicate-skip regression.
The previous test only checked the tracker advanced text; the
important identity contract is that the recalculated 5-tuple
matches the disk row, so a subsequent `removeLastUserMessage()`
removes the duplicate-skipped row rather than the older USER.
New test seeds disk with [first, second], stubs `_updateLogFile`
for the second call to mimic the duplicate-skip branch (mutate
newEntryObject's messageId+timestamp to align with the disk row,
return null), then asserts removeLastUserMessage() leaves
['first'] on disk and removes 'second'.
* fix(cli, core): close four cancel-auto-restore correctness gaps from PR review
Four critical findings from gpt-5.5 /review pass:
1. **Retry skipped the lastTurnUserItem reset** (useGeminiStream.ts)
`Retry` bypasses `prepareQueryForGemini`, which is where the
`lastTurnUserItemRef.current = null` reset lived. A retry that
followed a normal `UserQuery` carried the stale ownership snapshot
into `onCancelSubmit`, and cancelling the retry before any
meaningful output let `AppContainer` auto-restore truncate the
original failed prompt. Move the reset (and the new content-seen
reset, see #4) to the top of `submitQuery`, gated only on
"this is a top-level submit" — covers Retry, Cron, Notification,
and ordinary UserQuery alike.
2. **Text-only ownership matched dedup'd duplicates** (AppContainer.tsx,
useGeminiStream.ts) `useHistoryManager.addItem` skips inserting a
consecutive-duplicate user message while still returning a freshly
generated id. The text-only ownership check would match the OLDER
identical-text USER row, so a re-submitted same prompt + cancel
would wrongly truncate the prior turn. Carry id+text in
`CancelSubmitInfo.lastTurnUserItem` (using `addItem`'s return
value) and require both id AND text to match before truncating.
3. **stripOrphan left IDE context state advanced** (client.ts) Other
history-mutating paths (`setHistory`, `truncateHistory`) set
`forceFullIdeContext = true` after mutating; the orphan-strip
didn't, so a subsequent request could send a diff against a
removed baseline. Gate cache-clear + IDE-context invalidation on
an actual before/after length drop, so no-op strips don't churn
state.
4. **Flush-then-thought race let auto-restore wipe committed content**
(useGeminiStream.ts, AppContainer.tsx) `cancelOngoingRequest`'s
pre-cancel flush can `addItem` a meaningful `gemini_content` (via
handleContentEvent's split path) and then a later thought event
overwrites `pendingHistoryItem` with a synthetic value. The
AppContainer guard's React history snapshot is stale, so the
trailing-only-synthetic check passes and the just-committed text
gets truncated. Track a synchronous `turnSawContentEventRef` set
in handleContentEvent, ship it through `CancelSubmitInfo`, and
make the guard bail when set.
Tests:
- core/client.test.ts: stripOrphan only forces full IDE context on
actual removal; existing retry tests updated to mock
`getHistoryLength`.
- cli/useGeminiStream.test.tsx: ownership uses { id, text }, Retry
reset works after a prior UserQuery cancel,
turnProducedMeaningfulContent flips true when content lands.
- cli/AppContainer.test.tsx: guard bails on `turnProducedMeaningfulContent: true`,
guard bails on id mismatch (catches addItem dedup case).
cli 162/162 + core client 99/99 + core logger 51/51.
* fix(cli): repaint static transcript after auto-restore truncate
Reported by @tanzhenxin: auto-restore truncated React `history` state
but the cancelled `> prompt` and `Request cancelled.` lines stayed
printed in the terminal — Ink's `<Static>` region is append-only, so
shrinking the underlying array doesn't unprint already-flushed lines.
On the PR's golden path (type prompt → Enter → ESC) the user sees the
prompt twice: once in scrollback, once pre-filled in the input buffer.
Confirmed at multiple Enter-to-ESC delays, so it's not a timing fluke.
Call `refreshStatic()` immediately after `truncateToItem(...)` in the
auto-restore success path. `refreshStatic` writes the ANSI
clear-terminal escape AND bumps the static remount key — the exact
recipe `/clear` (`handleClearScreen`) already uses for the same
reason. The targeted-repaint helper used for terminal resizes is
intentionally NOT used here: it preserves scrollback, which would
leave the cancelled prompt visible above the new viewport.
Test: extend the existing auto-restore happy-path AppContainer test
to assert `mockStdout.write` was called with `ansiEscapes.clearTerminal`.
The other auto-restore-bail tests don't install the assertion so they
naturally verify the negative case (no clear when guard rejects).
|
||
|---|---|---|
| .github | ||
| .husky | ||
| .qwen | ||
| .vscode | ||
| docs | ||
| docs-site | ||
| eslint-rules | ||
| integration-tests | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .yamllint.yml | ||
| AGENTS.md | ||
| CONTRIBUTING.md | ||
| Dockerfile | ||
| esbuild.config.js | ||
| eslint.config.js | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
An open-source AI agent that lives in your terminal.
中文 | Deutsch | français | 日本語 | Русский | Português (Brasil)
🎉 News
-
2026-04-15: Qwen OAuth free tier has been discontinued. To continue using Qwen Code, switch to Alibaba Cloud Coding Plan, OpenRouter, Fireworks AI, or bring your own API key. Run
qwen authto configure. -
2026-04-13: Qwen OAuth free tier policy update: daily quota adjusted to 100 requests/day (from 1,000).
-
2026-04-02: Qwen3.6-Plus is now live! Get an API key from Alibaba Cloud ModelStudio to access it through the OpenAI-compatible API.
-
2026-02-16: Qwen3.5-Plus is now live!
Why Qwen Code?
Qwen Code is an open-source AI agent for the terminal, optimized for Qwen series models. It helps you understand large codebases, automate tedious work, and ship faster.
- Multi-protocol, flexible providers: use OpenAI / Anthropic / Gemini-compatible APIs, Alibaba Cloud Coding Plan, OpenRouter, Fireworks AI, or bring your own API key.
- Open-source, co-evolving: both the framework and the Qwen3-Coder model are open-source—and they ship and evolve together.
- Agentic workflow, feature-rich: rich built-in tools (Skills, SubAgents) for a full agentic workflow and a Claude Code-like experience.
- Terminal-first, IDE-friendly: built for developers who live in the command line, with optional integration for VS Code, Zed, and JetBrains IDEs.
Installation
Quick Install (Recommended)
Linux / macOS
bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)"
Windows (Run as Administrator)
Works in both Command Prompt and PowerShell:
powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')"
Note
: It's recommended to restart your terminal after installation to ensure environment variables take effect.
Manual Installation
Prerequisites
Make sure you have Node.js 22 or later installed. Download it from nodejs.org.
NPM
npm install -g @qwen-code/qwen-code@latest
Homebrew (macOS, Linux)
brew install qwen-code
Quick Start
# Start Qwen Code (interactive)
qwen
# Then, in the session:
/help
/auth
On first use, you'll be prompted to sign in. You can run /auth anytime to switch authentication methods.
Example prompts:
What does this project do?
Explain the codebase structure.
Help me refactor this function.
Generate unit tests for this module.
Click to watch a demo video
🦞 Use Qwen Code for Coding Tasks in Claw
Copy the prompt below and paste it into your agent:
Use Qwen Code as a coding agent. First, fetch and read this skill reference:
https://raw.githubusercontent.com/QwenLM/qwen-code/refs/heads/main/.qwen/skills/qwen-code-claw/SKILL.md
Then follow the instructions inside to install, authenticate, and use Qwen Code via acpx for all coding tasks.
Authentication
Qwen Code supports the following authentication methods:
- API Key (recommended): use an API key from Alibaba Cloud Model Studio (Beijing / intl) or any supported provider (OpenAI, Anthropic, Google GenAI, and other compatible endpoints).
- Coding Plan: subscribe to the Alibaba Cloud Coding Plan (Beijing / intl) for a fixed monthly fee with higher quotas.
⚠️ Qwen OAuth was discontinued on April 15, 2026. If you were previously using Qwen OAuth, please switch to one of the methods above. Run
qwenand then/authto reconfigure.
API Key (recommended)
Use an API key to connect to Alibaba Cloud Model Studio or any supported provider. Supports multiple protocols:
- OpenAI-compatible: Alibaba Cloud ModelStudio, ModelScope, OpenAI, OpenRouter, and other OpenAI-compatible providers
- Anthropic: Claude models
- Google GenAI: Gemini models
The recommended way to configure models and providers is by editing ~/.qwen/settings.json (create it if it doesn't exist). This file lets you define all available models, API keys, and default settings in one place.
Quick Setup in 3 Steps
Step 1: Create or edit ~/.qwen/settings.json
Here is a complete example:
{
"modelProviders": {
"openai": [
{
"id": "qwen3.6-plus",
"name": "qwen3.6-plus",
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"description": "Qwen3-Coder via Dashscope",
"envKey": "DASHSCOPE_API_KEY"
}
]
},
"env": {
"DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "qwen3.6-plus"
}
}
Step 2: Understand each field
| Field | What it does |
|---|---|
modelProviders |
Declares which models are available and how to connect to them. Keys like openai, anthropic, gemini represent the API protocol. |
modelProviders[].id |
The model ID sent to the API (e.g. qwen3.6-plus, gpt-4o). |
modelProviders[].envKey |
The name of the environment variable that holds your API key. |
modelProviders[].baseUrl |
The API endpoint URL (required for non-default endpoints). |
env |
A fallback place to store API keys (lowest priority; prefer .env files or export for sensitive keys). |
security.auth.selectedType |
The protocol to use on startup (openai, anthropic, gemini, vertex-ai). |
model.name |
The default model to use when Qwen Code starts. |
Step 3: Start Qwen Code — your configuration takes effect automatically:
qwen
Use the /model command at any time to switch between all configured models.
More Examples
Coding Plan (Alibaba Cloud ModelStudio) — fixed monthly fee, higher quotas
{
"modelProviders": {
"openai": [
{
"id": "qwen3.6-plus",
"name": "qwen3.6-plus (Coding Plan)",
"baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
"description": "qwen3.6-plus from ModelStudio Coding Plan",
"envKey": "BAILIAN_CODING_PLAN_API_KEY"
},
{
"id": "qwen3.5-plus",
"name": "qwen3.5-plus (Coding Plan)",
"baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
"description": "qwen3.5-plus with thinking enabled from ModelStudio Coding Plan",
"envKey": "BAILIAN_CODING_PLAN_API_KEY",
"generationConfig": {
"extra_body": {
"enable_thinking": true
}
}
},
{
"id": "glm-4.7",
"name": "glm-4.7 (Coding Plan)",
"baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
"description": "glm-4.7 with thinking enabled from ModelStudio Coding Plan",
"envKey": "BAILIAN_CODING_PLAN_API_KEY",
"generationConfig": {
"extra_body": {
"enable_thinking": true
}
}
},
{
"id": "kimi-k2.5",
"name": "kimi-k2.5 (Coding Plan)",
"baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
"description": "kimi-k2.5 with thinking enabled from ModelStudio Coding Plan",
"envKey": "BAILIAN_CODING_PLAN_API_KEY",
"generationConfig": {
"extra_body": {
"enable_thinking": true
}
}
}
]
},
"env": {
"BAILIAN_CODING_PLAN_API_KEY": "sk-xxxxxxxxxxxxx"
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "qwen3.6-plus"
}
}
Subscribe to the Coding Plan and get your API key at Alibaba Cloud ModelStudio(Beijing) or Alibaba Cloud ModelStudio(intl).
Multiple providers (OpenAI + Anthropic + Gemini)
{
"modelProviders": {
"openai": [
{
"id": "gpt-4o",
"name": "GPT-4o",
"envKey": "OPENAI_API_KEY",
"baseUrl": "https://api.openai.com/v1"
}
],
"anthropic": [
{
"id": "claude-sonnet-4-20250514",
"name": "Claude Sonnet 4",
"envKey": "ANTHROPIC_API_KEY"
}
],
"gemini": [
{
"id": "gemini-2.5-pro",
"name": "Gemini 2.5 Pro",
"envKey": "GEMINI_API_KEY"
}
]
},
"env": {
"OPENAI_API_KEY": "sk-xxxxxxxxxxxxx",
"ANTHROPIC_API_KEY": "sk-ant-xxxxxxxxxxxxx",
"GEMINI_API_KEY": "AIzaxxxxxxxxxxxxx"
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "gpt-4o"
}
}
Enable thinking mode (for supported models like qwen3.5-plus)
{
"modelProviders": {
"openai": [
{
"id": "qwen3.5-plus",
"name": "qwen3.5-plus (thinking)",
"envKey": "DASHSCOPE_API_KEY",
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
"generationConfig": {
"extra_body": {
"enable_thinking": true
}
}
}
]
},
"env": {
"DASHSCOPE_API_KEY": "sk-xxxxxxxxxxxxx"
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "qwen3.5-plus"
}
}
Tip: You can also set API keys via
exportin your shell or.envfiles, which take higher priority thansettings.json→env. See the authentication guide for full details.
Security note: Never commit API keys to version control. The
~/.qwen/settings.jsonfile is in your home directory and should stay private.
Local Model Setup (Ollama / vLLM)
You can also run models locally — no API key or cloud account needed. This is not an authentication method; instead, configure your local model endpoint in ~/.qwen/settings.json using the modelProviders field.
Set generationConfig.contextWindowSize inside the matching provider entry
and adjust it to the context length configured on your local server.
Ollama setup
- Install Ollama from ollama.com
- Pull a model:
ollama pull qwen3:32b - Configure
~/.qwen/settings.json:
{
"modelProviders": {
"openai": [
{
"id": "qwen3:32b",
"name": "Qwen3 32B (Ollama)",
"baseUrl": "http://localhost:11434/v1",
"description": "Qwen3 32B running locally via Ollama",
"generationConfig": {
"contextWindowSize": 131072
}
}
]
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "qwen3:32b"
}
}
vLLM setup
- Install vLLM:
pip install vllm - Start the server:
vllm serve Qwen/Qwen3-32B - Configure
~/.qwen/settings.json:
{
"modelProviders": {
"openai": [
{
"id": "Qwen/Qwen3-32B",
"name": "Qwen3 32B (vLLM)",
"baseUrl": "http://localhost:8000/v1",
"description": "Qwen3 32B running locally via vLLM",
"generationConfig": {
"contextWindowSize": 131072
}
}
]
},
"security": {
"auth": {
"selectedType": "openai"
}
},
"model": {
"name": "Qwen/Qwen3-32B"
}
}
Usage
As an open-source terminal agent, you can use Qwen Code in four primary ways:
- Interactive mode (terminal UI)
- Headless mode (scripts, CI)
- IDE integration (VS Code, Zed)
- SDKs (TypeScript, Python, Java)
Interactive mode
cd your-project/
qwen
Run qwen in your project folder to launch the interactive terminal UI. Use @ to reference local files (for example @src/main.ts).
Headless mode
cd your-project/
qwen -p "your question"
Use -p to run Qwen Code without the interactive UI—ideal for scripts, automation, and CI/CD. Learn more: Headless mode.
IDE integration
Use Qwen Code inside your editor (VS Code, Zed, and JetBrains IDEs):
SDKs
Build on top of Qwen Code with the available SDKs:
- TypeScript: Use the Qwen Code SDK
- Python: Use the Python SDK
- Java: Use the Java SDK
Python SDK example:
import asyncio
from qwen_code_sdk import is_sdk_result_message, query
async def main() -> None:
result = query(
"Summarize the repository layout.",
{
"cwd": "/path/to/project",
"path_to_qwen_executable": "qwen",
},
)
async for message in result:
if is_sdk_result_message(message):
print(message["result"])
asyncio.run(main())
Commands & Shortcuts
Session Commands
/help- Display available commands/clear- Clear conversation history/compress- Compress history to save tokens/stats- Show current session information/bug- Submit a bug report/exitor/quit- Exit Qwen Code
Keyboard Shortcuts
Ctrl+C- Cancel current operationCtrl+D- Exit (on empty line)Up/Down- Navigate command history
Learn more about Commands
Tip: In YOLO mode (
--yolo), vision switching happens automatically without prompts when images are detected. Learn more about Approval Mode
Configuration
Qwen Code can be configured via settings.json, environment variables, and CLI flags.
| File | Scope | Description |
|---|---|---|
~/.qwen/settings.json |
User (global) | Applies to all your Qwen Code sessions. Recommended for modelProviders and env. |
.qwen/settings.json |
Project | Applies only when running Qwen Code in this project. Overrides user settings. |
The most commonly used top-level fields in settings.json:
| Field | Description |
|---|---|
modelProviders |
Define available models per protocol (openai, anthropic, gemini, vertex-ai). |
env |
Fallback environment variables (e.g. API keys). Lower priority than shell export and .env files. |
security.auth.selectedType |
The protocol to use on startup (e.g. openai). |
model.name |
The default model to use when Qwen Code starts. |
See the Authentication section above for complete
settings.jsonexamples, and the settings reference for all available options.
Benchmark Results
Terminal-Bench Performance
| Agent | Model | Accuracy |
|---|---|---|
| Qwen Code | Qwen3-Coder-480A35 | 37.5% |
| Qwen Code | Qwen3-Coder-30BA3B | 31.3% |
Ecosystem
Looking for a graphical interface?
- AionUi A modern GUI for command-line AI tools including Qwen Code
- Gemini CLI Desktop A cross-platform desktop/web/mobile UI for Qwen Code
Troubleshooting
If you encounter issues, check the troubleshooting guide.
Common issues:
Qwen OAuth free tier was discontinued on 2026-04-15: Qwen OAuth is no longer available. Runqwen→/authand switch to API Key or Coding Plan. See the Authentication section above for setup instructions.
To report a bug from within the CLI, run /bug and include a short title and repro steps.
Connect with Us
- Discord: https://discord.gg/RN7tqZCeDK
- Dingtalk: https://qr.dingtalk.com/action/joingroup?code=v1,k1,+FX6Gf/ZDlTahTIRi8AEQhIaBlqykA0j+eBKKdhLeAE=&_dt_no_comment=1&origin=1
Acknowledgments
This project is based on Google Gemini CLI. We acknowledge and appreciate the excellent work of the Gemini CLI team. Our main contribution focuses on parser-level adaptations to better support Qwen-Coder models.
