Find a file
Shaojin Wen d07daa3e69
fix(cli): auto-restore prompt and preserve queue on cancel (#4023)
* 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).
2026-05-13 12:08:16 +08:00
.github ci(e2e): stabilize MCP/CLI flows and cancel stale main runs (#4039) 2026-05-12 16:09:30 +08:00
.husky Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
.qwen fix(cli): preserve table ANSI color across wrapped lines (#4050) 2026-05-12 16:09:39 +08:00
.vscode Merge branch 'main' into feat/sandbox-config-improvements 2026-03-06 14:38:39 +08:00
docs feat(cli): wrap markdown links in OSC 8 so wrapped URLs stay clickable (#4037) 2026-05-13 11:37:27 +08:00
docs-site feat: update docs 2025-12-15 09:47:03 +08:00
eslint-rules pre-release commit 2025-07-22 23:26:01 +08:00
integration-tests fix(cli): preserve table ANSI color across wrapped lines (#4050) 2026-05-12 16:09:39 +08:00
packages fix(cli): auto-restore prompt and preserve queue on cancel (#4023) 2026-05-13 12:08:16 +08:00
scripts feat(installer): add standalone archive installation (#3776) 2026-05-11 13:25:48 +08:00
.dockerignore fix(cli): skip stdin read for ACP mode 2026-03-27 11:47:01 +00:00
.editorconfig pre-release commit 2025-07-22 23:26:01 +08:00
.gitattributes pre-release commit 2025-07-22 23:26:01 +08:00
.gitignore feat(skills): Add codegraph skill for PR review risk analysis and conflict detection (#3910) 2026-05-11 18:14:40 +08:00
.npmrc chore: remove google registry 2025-08-08 20:45:54 +08:00
.nvmrc chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
.prettierignore Merge branch 'main' into feat/add-vscode-settings-json-schema 2026-03-03 11:21:57 +08:00
.prettierrc.json pre-release commit 2025-07-22 23:26:01 +08:00
.yamllint.yml Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
AGENTS.md chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
CONTRIBUTING.md chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
Dockerfile chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
esbuild.config.js chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
eslint.config.js feat(cli): add API preconnect to reduce first-call latency (#3318) 2026-04-27 06:54:55 +08:00
LICENSE Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
Makefile feat: update docs 2025-12-22 21:11:33 +08:00
package-lock.json revert(deps): downgrade ink 7 → 6 to fix Static-remount TUI regression from #3860 (#4083) 2026-05-12 17:11:17 +08:00
package.json revert(deps): downgrade ink 7 → 6 to fix Static-remount TUI regression from #3860 (#4083) 2026-05-12 17:11:17 +08:00
README.md chore(deps): upgrade ink 6.2.3 → 7.0.2 + bump Node engine to 22 (#3860) 2026-05-11 17:29:50 +08:00
SECURITY.md fix: update security vulnerability reporting channel 2026-02-24 14:22:47 +08:00
tsconfig.json # 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483) 2025-09-01 14:48:55 +08:00
vitest.config.ts test(channels): add comprehensive test suites for channel adapters 2026-03-27 15:26:39 +00:00

npm version License Node.js Version Downloads

QwenLM%2Fqwen-code | Trendshift

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 auth to 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

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 qwen and then /auth to reconfigure.

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 export in your shell or .env files, which take higher priority than settings.jsonenv. See the authentication guide for full details.

Security note: Never commit API keys to version control. The ~/.qwen/settings.json file 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
  1. Install Ollama from ollama.com
  2. Pull a model: ollama pull qwen3:32b
  3. 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
  1. Install vLLM: pip install vllm
  2. Start the server: vllm serve Qwen/Qwen3-32B
  3. 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:

  1. Interactive mode (terminal UI)
  2. Headless mode (scripts, CI)
  3. IDE integration (VS Code, Zed)
  4. 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:

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
  • /exit or /quit - Exit Qwen Code

Keyboard Shortcuts

  • Ctrl+C - Cancel current operation
  • Ctrl+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.json examples, 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. Run qwen/auth and 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

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.