qwen-code/packages/sdk-typescript
Shaojin Wen dade3ab334
feat(web-shell): add git branch picker, commit dialog, and create PR flow (#7731)
* feat(web-shell): add git branch picker, commit dialog, and create PR flow

Add an IntelliJ-style branch picker popover to the web shell git
workspace, accessible from the branch chip in both the composer toolbar
and sidebar. The picker provides search-filtered branch listing (local,
remote, tags, recent), branch checkout, new branch creation, pull, push,
and a commit view integrated into the existing GitDialog.

The commit view reuses the diff panel (expandable file diffs with syntax
highlighting, fullscreen support) and adds a commit message textarea with
Commit / Commit and Push buttons. When a session is available (or one is
auto-created), the commit message and PR title/body are generated via the
model using session side-queries (btwSession), giving the agent full
conversation context for accurate generation.

The Create PR flow provides an inline form with auto-detected base branch
and model-generated title/description, backed by a new daemon route that
shells out to gh pr create.

New daemon routes:
- GET  /workspaces/:workspace/git/branches
- POST /workspaces/:workspace/git/checkout
- POST /workspaces/:workspace/git/branch
- POST /workspaces/:workspace/git/push
- POST /workspaces/:workspace/git/pull
- POST /workspaces/:workspace/git/commit
- POST /workspaces/:workspace/github/prs/create
- GET  /workspaces/:workspace/github/default-branch

* fix(web-shell): resolve correct workspace session for AI generation

The commit message and PR title/body generation now resolves the
most recent session for the target workspace via listWorkspaceSessions,
rather than using the globally active connection.sessionId which may
belong to a different workspace or session. Falls back to creating a
new session only when no sessions exist for the workspace.

Also improves the PR body editor with an Edit/Preview toggle using
the existing Markdown component, and updates the generation prompt
to follow the project PR template structure from AGENTS.md.

* fix(web-shell): stabilize session resolver prop to prevent infinite re-generation

Pass resolveSessionForWorkspace as a stable useCallback reference
instead of an inline arrow function. The inline function created a
new reference on every App render, causing the GitDialog useEffect
to abort and restart generation in an infinite loop.

* fix(web-shell): group remote branches by remote name and add PR target branch dropdown

Remote branches in the branch picker are now grouped by remote
(origin, upstream, etc.) with sub-headers, making fork workflows
clear. The PR create form's base branch field is now a select
dropdown populated from the workspace's branch list, grouped by
remote with optgroup labels, instead of a free-text input.

* fix(web-shell): use ref for session resolver to prevent effect re-run abort

When resolveSessionForWorkspace creates a new session, it updates
connection.sessionId in the provider, which changes the useCallback
reference, which triggers the useEffect to re-run and abort the
in-flight btwSession generation. Store the callback in a ref so the
effect never depends on its identity.

* fix(web-shell): resolve session per workspace, not from global active session

The commit/PR generation effects used connection.sessionId directly
without checking if it belongs to the target workspace. When opening
the commit dialog from a sidebar workspace different from the active
session's workspace, the wrong session was used for generation,
producing incorrect content. Now always routes through
resolveSessionForWorkspace(workspaceCwd) which checks workspace
membership before reusing the active session.

Also replaces 'PR' with 'Pull Request' / '合并请求' in all UI strings
and adds error logging to the generation catch blocks.

* fix(web-shell): retry btwSession with fresh session when stale session detected

When listWorkspaceSessions returns a session that no longer exists in
the daemon's memory (e.g. after daemon restart), btwSession fails with
'No session with id ...'. The generation effects now catch this error,
force-create a new session via resolveSessionForWorkspace(cwd, true),
and retry the btwSession call once. Both btwWithRetry and
resolveSessionForWorkspace are stored in refs to avoid useEffect
dependency chain aborts.

* fix(web-shell): base PR generation on branch diff, not working tree

PR title/body generation now fetches the commit log between the
resolved base branch and HEAD (git log <base>..HEAD) plus any
uncommitted changes, instead of only the working tree diff. The
base branch is resolved inside the effect's promise chain (not
from state) to avoid stale values and dependency warnings. Also
adds range parameter support to fetchGitLog and workspaceGitLog.

* feat(web-shell): replace PR base branch select with searchable popover

The native <select> for the PR target branch is replaced with a
custom searchable popover (BranchSelect) that shows a search input
and a filtered branch list grouped by remote. The default selection
is the target repository's main branch (resolved via
getDefaultBranch). Supports filtering by typing in the search box.

* fix(web-shell): show full remote ref in branch select (origin/main)

Branch select now stores and displays full remote refs like
origin/main instead of stripped names. getDefaultBranch returns
the full ref (origin/main) instead of stripping the prefix.
When creating the PR, the remote prefix is stripped for the
gh pr create --base flag (origin/main → main).

* fix(web-shell): stop pointer propagation in branch picker list to prevent popover dismiss

Radix Popover's outside-click detection was incorrectly firing when
clicking section headers (Recent/Local/Remote/Tags) inside the popover
content, causing the popover to close immediately. Adding
onPointerDown stopPropagation on the list container prevents pointer
events from reaching Radix's document-level handlers.

* fix(web-shell): use onPointerDownOutside guard for branch picker popover

The previous stopPropagation approach failed because Radix Popover
uses capture-phase document listeners for outside-click detection,
which fire before bubble-phase stopPropagation. The correct fix is
onPointerDownOutside on PopoverContent: when Radix incorrectly fires
the outside handler for a click that is actually inside the content
(can happen with portal containers), we check contentRef.contains()
and preventDefault to keep the popover open.

* fix(web-shell): stop click propagation on branch picker popover content

Root cause: the ChatEditor composer container has onClick that calls
core.focus(), stealing focus from the popover. React synthetic events
bubble through the React tree (not DOM tree), so portaled popover
clicks reach the container handler. Radix then detects focus-outside
and dismisses the popover.

Fix: onClick stopPropagation on PopoverContent, matching the existing
pattern in GitModePopover and ToolbarPopover which already have this
fix with an explanatory comment.

* docs: add PR verification screenshots for branch picker feature

* fix(web-shell): mock useWorkspace in tests for BranchPickerPopover

BranchPickerPopover calls useWorkspace() which requires
DaemonWorkspaceProvider context. The existing WorkspaceSection and
ChatEditor tests didn't provide this context, causing 5 test failures.
Added vi.mock with importActual to preserve other exports while
providing a mock useWorkspace. Also updated the git chip click test
to reflect that clicking now opens the branch picker popover instead
of directly calling onOpenGitDiff.

* fix: harden git write paths against argument injection

Address review feedback on the web-shell git surface:

- Reject option/pathspec injection in git checkout ref and branch start
  point (isValidCheckoutRef), and terminate `git checkout` argv with `--`.
- Drop `git log` range values that start with `-` and terminate the argv
  with `--` so a range can never be reinterpreted as `--output=<file>`.
- Fix getDefaultBranch always falling back to origin/main: the promisified
  exec lacked `encoding: 'utf8'`, so stdout was a Buffer and .trim() threw.
- Parameterize ghErrorMessage so `gh pr create` timeouts name the right
  command and duration; sanitize workspace paths in PR-create errors.
- GitDialog: guard doCommit against double-submit (button + keyboard), and
  strip only a known remote prefix from the PR base so local branches with
  "/" are not mangled; use theme tokens for commit button/success colors.
- BranchPickerPopover: guard checkout/new-branch behind busyAction, reset
  inline-input text on reopen, and hide the commit action when unavailable.

Adds regression tests for the checkout/branch validation and the git log
range guard.

* fix(web-shell): address review feedback on branch picker PR (#7731)

- Fix Commit+Push error masking: split try/catch so push failure
  reports alongside the successful commit SHA
- Replace hardcoded screenshot path with captureScreenshot harness
- Replace silent if-isVisible skip with explicit assertion in visual test
- Add focus-visible style for search input accessibility
- Fix CSS specificity for active PR tab hover state
- Add viewChanges to actionsVisible search filter
- Wrap toggleSection in useCallback to avoid unnecessary re-renders
- Add windowsHide: true to getDefaultBranch subprocess
- Fix trailing slash handling in mockDaemon git action routing
- Add git methods to top-level client mock in tests
- Remove dead branchPicker.commitSuccess i18n key
- Remove dead .actionShortcut CSS class
- Show generation failure feedback in commit message placeholder

* fix(web-shell): address review feedback on branch picker PR (#7731)

- Add workspace trust checks to bound git branch routes
- Use workspace-scoped client in BranchPickerPopover (fixes wrong-workspace mutation)
- Add branch name validation and -- terminator to gitCreateBranch
- Filter refs/remotes/*/HEAD from branch listings
- Force LC_ALL=C for reflog parsing (non-English locale fix)
- Narrow 'could not resolve' error regex to avoid DNS false positives
- Add range validation to git log (reject path traversal)
- Fix commit+push error i18n (dedicated key instead of concatenation)
- Add i18n for BranchSelect component strings
- Fix commit tab ARIA attributes
- Add onBranchChanged callback to handlePush
- Add btw to mockDaemon isDaemonPath regex
- Add workspace_github_prs to visuals spec capabilities
- Add -- terminator regression test
- Remove docs/pr-assets/ from repo

* fix(web-shell): address review feedback on branch picker PR (#7731)

* fix(cli): reject dash-prefixed branch name with 400 in branch route (#7731)

* fix(web-shell): address review feedback on git branch picker (#7731)

Security:
- Clear GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE from git
  subprocess env to prevent repository redirection
- Add strict mutation gate to all POST git branch routes
- Add generation guard to qualified write routes
- Fail closed on invalid ?cwd= in mutation routes (resolveContainedCwdOrFail)
- Reject wrong-typed startPoint, fetchOnly, rebase, and PR options with 400

Correctness:
- Filter remote symbolic refs (origin/HEAD) by %(symref) instead of /HEAD
  name suffix, preserving valid branches like feature/HEAD
- Add git rev-parse --git-dir probe so non-git dirs get 404 instead of
  empty available:true
- Push preserves existing upstream; only adds --set-upstream when unset,
  resolving the remote from branch config or the sole configured remote
- git commit -a replaced with git add -A + git commit so untracked files
  displayed in the UI are included
- Always pass --body to gh pr create to prevent interactive prompts
- getDefaultBranch returns null instead of fabricating origin/main
- Memoize workspaceByCwd client in BranchPickerPopover to fix infinite
  render loop
- Move sessionId to a ref in GitDialog effects to prevent self-abort
- Bound commit-message prompt to fit /btw 4096-char limit
- Mark all platforms as unverified in PR template (no fabricated )
- Guard PR auto-fill effect against wiping user edits on reconnect

Accessibility:
- Add tabIndex and onKeyDown to commit-mode tab span
- Add aria-label to BranchSelect trigger and search input

Cleanup:
- Remove dead CSS (.prInputSmall, .prSelect)
- Remove 9 unused i18n keys
- Add ^ to git log range validation regex
- Add busyAction guard to handlePush/handlePull
- Add unit tests for gitCommit, gitPull, and route input validation

* fix(web-shell): address review feedback on git branch picker PR (#7731)

- Change commit tab from <span> to <button> for keyboard accessibility
- Move setCommitMsg('') to success-only branches so the message is
  preserved when push fails after a successful commit
- Add mutate middleware and generationGuard to PR creation route,
  matching all other POST mutation routes
- Set genFailed when session resolution returns undefined so the user
  sees the failure indicator instead of a silent empty textarea
- Make PR number nullable when URL regex does not match instead of
  returning a misleading 0
- Add LC_ALL=C and LANG=C to gitEnv() so for-each-ref upstream track
  parsing is locale-independent
- Validate setUpstream and force as booleans in handlePush, matching
  the existing validation in handlePull
- Add missing workspaceCwd and available fields to test mocks
- Use stable data-web-shell-git-branch attribute in e2e selector

* fix(web-shell): address R5 review feedback on git branch picker PR (#7731)

- Classify git errors on stdout+stderr instead of err.message to fix
  false-positive no_upstream on every push failure and dead
  nothing_to_commit classifier
- Sanitize workspace paths and cap error message length in sendGitError
- Fix remote branch checkout to strip remote prefix so git DWIM creates
  a local tracking branch instead of detaching HEAD
- Restore keyboard accessibility on composer branch chip (span → button)
- Trim startPoint in handleCreateBranch before forwarding to git
- Return bare branch name from getDefaultBranch (strip remote prefix)
- Fix i18n shortcut hint to show ⌘/Ctrl+Enter for cross-platform
- Update aria-label to reflect git management menu, not just changes
- Add available: true to mockDaemon gitDiff default payload
- Add regression tests: upstream preservation, sole remote resolution,
  strengthened fetch-only with divergent remote commit
- Add aria-expanded assertion to sidebar picker test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R7 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R8 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R9 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R10 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R11 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R12 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R13 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R14 review feedback on git branch picker PR (#7731)

* fix(web-shell): address R15 review feedback on git branch picker PR (#7731)

- Validate localName derived from remote-tracking ref to prevent option
  injection (e.g. origin/-f → git checkout -f)
- Add gitCwd prop to BranchPickerPopover and pass it to all git SDK calls
  so worktree sessions target the correct directory
- Add symlink-escape and non-existent-path tests for resolveContainedCwdOrFail
- Pin initial branch name in makeRepo() with git init -b master
- Add gitPull merge and rebase integration tests

* fix(web-shell): address git branch picker review feedback (#7731)

* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)

* fix(web-shell): address review feedback on git branch picker PR (#7731)

- Strip GIT_CONFIG_GLOBAL/SYSTEM/NOSYSTEM in gitEnv to prevent
  inherited config redirection (consistent with extension/github.ts)
- Pass gitCwd to workspaceGitBranches in GitDialog loadPrBranches
  so worktree sessions fetch branches from the correct repository
- Add aria-expanded to collapsible branch section headers
- Add happy-path tests for PR create (201) and default-branch (200)
  routes, including the null fallback to origin/main

* fix(cli): add sendGenerationClosedError to POST routes and cover untested branches (#7731)

* fix(web-shell): address review feedback on branch picker and PR creation (#7731)

- Refresh branch list after push/pull to avoid stale ahead/behind counts
- Add pre-flight check for unpushed branches before PR creation
- Fix base branch prefix stripping when branch list is unavailable
- Cap PR body file list at MAX_SUMMARY_CHARS to bound model prompt size
- Add qualified route tests: trust guard, input validation, cwd containment

* fix(web-shell): hoist MAX_SUMMARY_CHARS to module scope for PR body generation (#7731)

* fix(web-shell): address review feedback for git branch picker (#7731)

- Hoist onOpenCommit to useCallback to fix App.test.tsx prop stability test
- Keep commit tab visible after navigating away (startedInCommit ref)
- Add onClick handler to commit tab for navigation back to commit view
- Fix branch-prefix strip mangling local branch names containing '/'
- Update sessionIdRef after force-creating a stale session replacement
- Pin core.hooksPath in test makeRepo for reliable rollback tests
- Add test asserting --force-with-lease is used for force pushes

* fix(web-shell): target the worktree for sidebar commits and harden git actions (#7731)

Scope the sidebar commit dialog to the active session's worktree checkout
(matching the composer path) so linked-worktree sessions commit to the right
checkout, guard PR creation against a double-click race, and surface an error
when an invalid branch name is submitted. Adds focused coverage for the branch
picker action wiring and the git branch route validation paths.

* fix(web-shell): address review feedback for git branch picker (#7731)

---------

Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
2026-07-28 14:25:11 +00:00
..
scripts feat(web-shell): add git branch picker, commit dialog, and create PR flow (#7731) 2026-07-28 14:25:11 +00:00
src feat(web-shell): add git branch picker, commit dialog, and create PR flow (#7731) 2026-07-28 14:25:11 +00:00
test feat(web-shell): add git branch picker, commit dialog, and create PR flow (#7731) 2026-07-28 14:25:11 +00:00
package.json feat(webshell): replay ChatRecord history in readonly WebShell (#6999) 2026-07-19 00:09:44 +00:00
README.md feat(web-shell): show subagent sessions in detail panel (#7380) 2026-07-22 02:31:08 +00:00
tsconfig.build.json chore: keep comments for queryOptions 2025-12-04 17:10:21 +08:00
tsconfig.json Add Gemini provider, remove legacy Google OAuth, and tune generation defaults 2025-12-19 16:26:54 +08:00
tsconfig.reference.json feat(cli): Add daemon-managed channel worker for serve --channel (#6031) 2026-07-01 01:40:54 +00:00
vitest.config.ts feat(webshell): replay ChatRecord history in readonly WebShell (#6999) 2026-07-19 00:09:44 +00:00

@qwen-code/sdk

A minimum experimental TypeScript SDK for programmatic access to Qwen Code.

Feel free to submit a feature request/issue/PR.

Installation

npm install @qwen-code/sdk

Requirements

  • Node.js >= 22.0.0

From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.

Quick Start

import { query } from '@qwen-code/sdk';

// Single-turn query
const result = query({
  prompt: 'What files are in the current directory?',
  options: {
    cwd: '/path/to/project',
  },
});

// Iterate over messages
for await (const message of result) {
  if (message.type === 'assistant') {
    console.log('Assistant:', message.message.content);
  } else if (message.type === 'result') {
    console.log('Result:', message.result);
  }
}

API Reference

query(config)

Creates a new query session with the Qwen Code.

Parameters

  • prompt: string | AsyncIterable<SDKUserMessage> - The prompt to send. Use a string for single-turn queries or an async iterable for multi-turn conversations.
  • options: QueryOptions - Configuration options for the query session.

QueryOptions

Option Type Default Description
cwd string process.cwd() The working directory for the query session. Determines the context in which file operations and commands are executed.
model string - The AI model to use (e.g., 'qwen-max', 'qwen-plus', 'qwen-turbo'). Takes precedence over OPENAI_MODEL and QWEN_MODEL environment variables.
pathToQwenExecutable string Auto-detected Path to the Qwen Code executable. Supports multiple formats: 'qwen' (native binary from PATH), '/path/to/qwen' (explicit path), '/path/to/cli.js' (Node.js bundle), 'node:/path/to/cli.js' (force Node.js runtime), 'bun:/path/to/cli.js' (force Bun runtime). If not provided, auto-detects from: QWEN_CODE_CLI_PATH env var, ~/.volta/bin/qwen, ~/.npm-global/bin/qwen, /usr/local/bin/qwen, ~/.local/bin/qwen, ~/node_modules/.bin/qwen, ~/.yarn/bin/qwen.
permissionMode 'default' | 'plan' | 'auto-edit' | 'auto' | 'yolo' 'default' Permission mode controlling tool execution approval. See Permission Modes for details.
canUseTool CanUseTool - Custom permission handler for tool execution approval. Invoked when a tool requires confirmation. Must respond within 60 seconds or the request will be auto-denied. See Custom Permission Handler.
env Record<string, string> - Environment variables to pass to the Qwen Code process. Merged with the current process environment.
systemPrompt string | QuerySystemPromptPreset - System prompt configuration for the main session. Use a string to fully override the built-in Qwen Code system prompt, or a preset object to keep the built-in prompt and append extra instructions.
mcpServers Record<string, McpServerConfig> - MCP (Model Context Protocol) servers to connect. Supports external servers (stdio/SSE/HTTP) and SDK-embedded servers. External servers are configured with transport options like command, args, url, httpUrl, etc. SDK servers use { type: 'sdk', name: string, instance: Server }.
abortController AbortController - Controller to cancel the query session. Call abortController.abort() to terminate the session and cleanup resources.
debug boolean false Enable debug mode for verbose logging from the CLI process.
maxSessionTurns number -1 (unlimited) Maximum number of conversation turns before the session automatically terminates. Must be an integer. A turn consists of a user message and an assistant response.
coreTools string[] - Uses the legacy coreTools / CLI --core-tools allowlist semantics. If specified, only matching core tools are registered for the session. This is separate from permissions.allow, which auto-approves matching tool calls but does not restrict tool registration. Example: ['read_file', 'edit', 'run_shell_command'].
excludeTools string[] - Equivalent to permissions.deny in settings.json. Excluded tools return a permission error immediately. Takes highest priority over all other permission settings. Supports tool name aliases and pattern matching: tool name ('write_file'), shell command prefix ('Bash(rm *)'), or path patterns ('Read(.env)', 'Edit(/src/**)').
allowedTools string[] - Equivalent to permissions.allow in settings.json. Matching tools bypass canUseTool callback and execute automatically. Only applies when tool requires confirmation. Supports same pattern matching as excludeTools. Example: ['Bash(git status)', 'Bash(npm test)'].
authType 'openai' | 'qwen-oauth' 'openai' Authentication type for the AI service. Qwen OAuth free tier was discontinued on 2026-04-15; new SDK setups should use OpenAI-compatible authentication or another supported provider.
agents SubagentConfig[] - Configuration for subagents that can be invoked during the session. Subagents are specialized AI agents for specific tasks or domains.
includePartialMessages boolean false When true, the SDK emits incomplete messages as they are being generated, allowing real-time streaming of the AI's response.
resume string - Resume a previous session by providing its session ID. Equivalent to CLI's --resume flag.
sessionId string - Specify a session ID for the new session. Ensures SDK and CLI use the same ID without resuming history. Equivalent to CLI's --session-id flag.

Tip

If you need to configure coreTools, excludeTools, or allowedTools, it is strongly recommended to read the permissions configuration documentation first, especially the Tool name aliases and Rule syntax examples sections. Rule patterns such as Bash(git *), Read(.env), and Edit(/src/**) apply to excludeTools and allowedTools; coreTools accepts aliases but strips invocation specifiers.

Timeouts

The SDK enforces the following default timeouts:

Timeout Default Description
canUseTool 1 minute Maximum time for canUseTool callback to respond. If exceeded, the tool request is auto-denied.
mcpRequest 1 minute Maximum time for SDK MCP tool calls to complete.
controlRequest 1 minute Maximum time for control operations like initialize(), setModel(), setPermissionMode(), getContextUsage(), and interrupt() to complete.
streamClose 1 minute Maximum time to wait for initialization to complete before closing CLI stdin in multi-turn mode with SDK MCP servers.

You can customize these timeouts via the timeout option:

const query = qwen.query('Your prompt', {
  timeout: {
    canUseTool: 60000, // 60 seconds for permission callback
    mcpRequest: 600000, // 10 minutes for MCP tool calls
    controlRequest: 60000, // 60 seconds for control requests
    streamClose: 15000, // 15 seconds for stream close wait
  },
});

Experimental Daemon Session Client

DaemonSessionClient is an experimental wrapper for clients that talk to a running qwen serve daemon over HTTP + SSE. It binds one daemon session so TUI, channel, IDE, or web backend adapters do not need to pass sessionId into every call.

import { DaemonClient, DaemonSessionClient } from '@qwen-code/sdk';

const daemon = new DaemonClient({
  baseUrl: 'http://127.0.0.1:4170',
  token: process.env['QWEN_SERVER_TOKEN'],
});

const caps = await daemon.capabilities();
const session = await DaemonSessionClient.createOrAttach(daemon, {
  workspaceCwd: caps.workspaceCwd,
});

const eventController = new AbortController();
const eventTask = (async () => {
  for await (const event of session.events({
    signal: eventController.signal,
  })) {
    console.log(event.type, event.data);
  }
})();

const result = await session.prompt({
  prompt: [{ type: 'text', text: 'Summarize this workspace.' }],
});

eventController.abort();
await eventTask;
console.log(result.stopReason);

session.events() tracks the last seen SSE event id and reuses it on the next subscription by default. Pass { resume: false } to start a fresh subscription without sending Last-Event-ID.

When createOrAttach() is called with modelServiceId, the returned session client seeds its first event subscription with Last-Event-ID: 0. This replays the daemon ring from the oldest available event so adapters can observe attach-time model_switch_failed or model_switched events that are not reported on the create/attach HTTP response. Raw DaemonClient callers should pass { lastEventId: 0 } on their first subscribeEvents() call when they use modelServiceId.

The raw event envelope remains available as DaemonEvent with data: unknown. Adapters that want a v1 typed view can layer the schema helpers on top without changing the wire stream:

import {
  asKnownDaemonEvent,
  createDaemonSessionViewState,
  reduceDaemonSessionEvent,
} from '@qwen-code/sdk';

let view = createDaemonSessionViewState();
for await (const event of session.events()) {
  view = reduceDaemonSessionEvent(view, event);

  const known = asKnownDaemonEvent(event);
  if (known?.type === 'permission_request') {
    console.log(known.data.requestId);
  }
}

Offline daemon transcript projection

The opt-in browser-safe transcript entry converts already-parsed append-only ChatRecord values into the same block model used by daemon clients. It does not read files, parse JSONL text, start a daemon, or access network or browser storage.

import { projectChatRecordsToDaemonTranscript } from '@qwen-code/sdk/daemon/transcript';

const projection = projectChatRecordsToDaemonTranscript(records);
if (!projection.complete) {
  console.warn(projection.diagnostics);
}
renderTranscript(projection.blocks);

Projection is synchronous and scans all records and message parts. maxBlocks limits retained output blocks, not computation. As a conservative browser guideline, project up to roughly 1,000 ordinary records on the main thread; move larger or unusually text-heavy inputs to a Web Worker and call the same entry there.

Message Types

The SDK provides type guards to identify different message types:

import {
  isSDKUserMessage,
  isSDKAssistantMessage,
  isSDKSystemMessage,
  isSDKResultMessage,
  isSDKPartialAssistantMessage,
} from '@qwen-code/sdk';

for await (const message of result) {
  if (isSDKAssistantMessage(message)) {
    // Handle assistant message
  } else if (isSDKResultMessage(message)) {
    // Handle result message
  }
}

Query Instance Methods

The Query instance returned by query() provides several methods:

const q = query({ prompt: 'Hello', options: {} });

// Get session ID
const sessionId = q.getSessionId();

// Check if closed
const closed = q.isClosed();

// Interrupt the current operation
await q.interrupt();

// Change permission mode mid-session
await q.setPermissionMode('yolo');

// Change model mid-session
await q.setModel('qwen-max');

// Get context window usage breakdown (token counts per category)
const usage = await q.getContextUsage();
// Pass true to hint that per-item details should be displayed
const detail = await q.getContextUsage(true);

// Close the session
await q.close();

Permission Modes

The SDK supports different permission modes for controlling tool execution:

  • default: Write tools are denied unless approved via canUseTool callback or in allowedTools. Read-only tools execute without confirmation.
  • plan: Blocks all write tools, instructing AI to present a plan first.
  • auto-edit: Auto-approve edit tools (edit, write_file, notebook_edit) while other tools require confirmation.
  • auto: Uses the built-in classifier to auto-approve safe tool calls and block risky ones, with manual-approval fallback after repeated policy blocks or classifier outages.
  • yolo: All tools execute automatically without confirmation.

Permission Priority Chain

Decision priority (highest first): deny > ask > allow > (default/interactive mode)

The first matching rule wins.

  1. excludeTools / permissions.deny - Blocks tools completely (returns permission error)
  2. permissions.ask - Always requires user confirmation
  3. permissionMode: 'plan' - Blocks all non-read-only tools
  4. permissionMode: 'yolo' - Auto-approves all tools
  5. allowedTools / permissions.allow - Auto-approves matching tools
  6. permissionMode: 'auto' - Classifier-mediated approval for remaining tools
  7. canUseTool callback - Custom approval logic (if provided, not called for allowed tools)
  8. Default behavior - Auto-deny in SDK mode (write tools require explicit approval)

Examples

Multi-turn Conversation

import { query, type SDKUserMessage } from '@qwen-code/sdk';

async function* generateMessages(): AsyncIterable<SDKUserMessage> {
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Create a hello.txt file' },
    parent_tool_use_id: null,
  };

  // Wait for some condition or user input
  yield {
    type: 'user',
    session_id: 'my-session',
    message: { role: 'user', content: 'Now read the file back' },
    parent_tool_use_id: null,
  };
}

const result = query({
  prompt: generateMessages(),
  options: {
    permissionMode: 'auto-edit',
  },
});

for await (const message of result) {
  console.log(message);
}

Custom Permission Handler

import { query, type CanUseTool } from '@qwen-code/sdk';

const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
  // Allow all read operations
  if (toolName.startsWith('read_')) {
    return { behavior: 'allow', updatedInput: input };
  }

  // Prompt user for write operations (in a real app)
  const userApproved = await promptUser(`Allow ${toolName}?`);

  if (userApproved) {
    return { behavior: 'allow', updatedInput: input };
  }

  return { behavior: 'deny', message: 'User denied the operation' };
};

const result = query({
  prompt: 'Create a new file',
  options: {
    canUseTool,
  },
});

Handling ask_user_question

When the model needs a decision from the user it calls the built-in ask_user_question tool. The SDK surfaces this through the same canUseTool callback: the tool input contains a questions array, and you return the collected answers via updatedInput.answers. answers is an object keyed by the question's index (as a string), where each value is the label of the chosen option (or free-form text when the user picks "Other").

import { query, type CanUseTool } from '@qwen-code/sdk';

const canUseTool: CanUseTool = async (toolName, input, { signal }) => {
  if (toolName === 'ask_user_question') {
    const questions = input.questions as Array<{
      question: string;
      header: string;
      options: Array<{ label: string; description: string }>;
    }>;

    // Present the questions to the user however your app sees fit, then
    // build an index-keyed map of their answers.
    const answers: Record<string, string> = {};
    for (let i = 0; i < questions.length; i++) {
      answers[String(i)] = await promptUserToChoose(questions[i]);
    }

    // Return the answers through `updatedInput.answers` — the CLI forwards
    // them to the tool so the model receives the user's decisions.
    return { behavior: 'allow', updatedInput: { ...input, answers } };
  }

  return { behavior: 'allow', updatedInput: input };
};

If you return allow without any answers, the tool reports that no answer was provided; return deny to signal the user declined.

With External MCP Servers

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Use the custom tool from my MCP server',
  options: {
    mcpServers: {
      'my-server': {
        command: 'node',
        args: ['path/to/mcp-server.js'],
        env: { PORT: '3000' },
      },
    },
  },
});

Override the System Prompt

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Say hello in one sentence.',
  options: {
    systemPrompt: 'You are a terse assistant. Answer in exactly one sentence.',
  },
});

Append to the Built-in System Prompt

import { query } from '@qwen-code/sdk';

const result = query({
  prompt: 'Review the current directory.',
  options: {
    systemPrompt: {
      type: 'preset',
      preset: 'qwen_code',
      append: 'Be terse and focus on concrete findings.',
    },
  },
});

With SDK-Embedded MCP Servers

The SDK provides tool and createSdkMcpServer to create MCP servers that run in the same process as your SDK application. This is useful when you want to expose custom tools to the AI without running a separate server process.

tool(name, description, inputSchema, handler)

Creates a tool definition with Zod schema type inference.

Parameter Type Description
name string Tool name (1-64 chars, starts with letter, alphanumeric and underscores)
description string Human-readable description of what the tool does
inputSchema ZodRawShape Zod schema object defining the tool's input parameters
handler (args, extra) => Promise<Result> Async function that executes the tool and returns MCP content blocks

The handler must return a CallToolResult object with the following structure:

{
  content: Array<
    | { type: 'text'; text: string }
    | { type: 'image'; data: string; mimeType: string }
    | { type: 'resource'; uri: string; mimeType?: string; text?: string }
  >;
  isError?: boolean;
}

createSdkMcpServer(options)

Creates an SDK-embedded MCP server instance.

Option Type Default Description
name string Required Unique name for the MCP server
version string '1.0.0' Server version
tools SdkMcpToolDefinition[] - Array of tools created with tool()

Returns a McpSdkServerConfigWithInstance object that can be passed directly to the mcpServers option.

Example

import { z } from 'zod';
import { query, tool, createSdkMcpServer } from '@qwen-code/sdk';

// Define a tool with Zod schema
const calculatorTool = tool(
  'calculate_sum',
  'Add two numbers',
  { a: z.number(), b: z.number() },
  async (args) => ({
    content: [{ type: 'text', text: String(args.a + args.b) }],
  }),
);

// Create the MCP server
const server = createSdkMcpServer({
  name: 'calculator',
  tools: [calculatorTool],
});

// Use the server in a query
const result = query({
  prompt: 'What is 42 + 17?',
  options: {
    permissionMode: 'yolo',
    mcpServers: {
      calculator: server,
    },
  },
});

for await (const message of result) {
  console.log(message);
}

Abort a Query

import { query, isAbortError } from '@qwen-code/sdk';

const abortController = new AbortController();

const result = query({
  prompt: 'Long running task...',
  options: {
    abortController,
  },
});

// Abort after 5 seconds
setTimeout(() => abortController.abort(), 5000);

try {
  for await (const message of result) {
    console.log(message);
  }
} catch (error) {
  if (isAbortError(error)) {
    console.log('Query was aborted');
  } else {
    throw error;
  }
}

Error Handling

The SDK provides an AbortError class for handling aborted queries:

import { AbortError, isAbortError } from '@qwen-code/sdk';

try {
  // ... query operations
} catch (error) {
  if (isAbortError(error)) {
    // Handle abort
  } else {
    // Handle other errors
  }
}

FAQ / Troubleshooting

Version 0.1.0 Requirements

If you're using SDK version 0.1.0, please note the following requirements:

Qwen Code Installation Required

Version 0.1.0 requires Qwen Code >= 0.4.0 to be installed separately and accessible in your PATH.

# Install Qwen Code globally
npm install -g @qwen-code/qwen-code@latest

Note: From version 0.1.1 onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed.

License

Apache-2.0 - see LICENSE for details.