* feat(core): emit liveness heartbeats for silent foreground shell commands Silent foreground commands previously produced no events between spawn and settle, so ACP gateways and stream-json consumers could not tell a long-running command from a dead session. The shell tool now emits a structured ShellProgressData through the existing updateOutput channel whenever no display update has fired for tools.shell.heartbeatIntervalMs (default 10s, 0 disables). Heartbeats carry liveness stats only - never command output - and never enter model context. Consumers: the ACP session forwards heartbeats as meta-only tool_call_update frames (gated so a tick racing the settle path cannot regress status after completion) and records heartbeat span attributes; stream-json forwards them as tool_progress events behind includePartialMessages; the TUI scheduler, React hook, and subagent runtime ignore them so live output views are not replaced by stats objects. * docs(design): add silent command heartbeat design doc * fix(acp): keep tool_call_update heartbeats from breaking in-repo consumers Codex review of the heartbeat change found that in-repo ACP consumers did not tolerate the new meta-only in_progress frames. A full sweep of tool_call_update consumers found three that mishandled them, each now guarded with a regression test: - The desktop agent converted every tool_call_update into a terminal tool_result, so the first heartbeat would prematurely complete the command with an empty result. It now skips in_progress updates. - DaemonChannelBridge requires kind on tool_call_update and flagged the kind-less heartbeat as a malformed-protocol error every interval. It now drops kind-less in_progress frames silently. - The web-shell daemon UI normalizer derived the tool block title from _meta.toolName, overwriting the human-readable title on every heartbeat. It now drops heartbeat frames outright. The remaining consumers (VS Code companion, acp-bridge compaction, session export, daemon TUI adapter) merge updates conditionally and are heartbeat-safe without changes. * fix(core): address PR review — heartbeat monotonic gate, guard scope, telemetry Review round 1 on #6876 (yiliang114, wenshao, chiga0, qwen3.7-max): - shell.ts: the silent-idle gate now uses the monotonic performance.now() clock (via lastOutputPerfTime, falling back to spawn time) instead of the Date.now()-based lastUpdateTime, so an NTP step can neither skew the payload nor misfire a heartbeat — matching the design doc's monotonic commitment. It also keys off actual output arrival rather than the throttled display update. - session-tracing.ts: endToolExecutionSpan now applies caller-supplied attributes BEFORE the canonical keys (duration_ms, success, error) so a passthrough attribute can never mask the span's own outcome fields. - desktop qwen-agent.ts: the in_progress drop guard is now scoped to frames carrying _meta.shellProgress, matching the daemon bridge and web-shell normalizer guards, so a future non-heartbeat in_progress frame is not silently swallowed. - Tests: the desktop regression test now pins result==='done' (previously it stayed green even with the guard removed); added a Session.test assertion that heartbeat counts reach the tool-execution span attributes. * fix(acp): align desktop heartbeat guard with normalizer; test kind pass-through Review round 2 on #6876 (qwen3.7-max via ci-bot): - The desktop qwen-agent in_progress drop guard was broader than the web-shell normalizer's: it dropped any in_progress + shellProgress frame regardless of kind, while the normalizer only drops kind-less ones. The comment claimed they matched. Added the kind-absent check so the desktop guard matches the normalizer exactly — a kind-bearing frame now passes through on both platforms (heartbeats emitted by the ACP session never carry a kind, so real behavior is unchanged). - Added pass-through tests on both sides (daemonUi + desktop) asserting an in_progress frame WITH a kind normalizes to a tool.update / tool_result rather than being dropped, so the load-bearing kind-absent condition is no longer only exercised on the drop path. * fix(channels): scope daemon bridge heartbeat drop to shellProgress frames Review round 3 on #6876 (qwen3.7-max via ci-bot): the DaemonChannelBridge heartbeat guard lived in the shared tool_call / tool_call_update case and dropped ANY kind-less in_progress frame, so a genuinely malformed kind-less tool_call (status in_progress, no shellProgress) was silently swallowed instead of reaching emitProtocolError. Gate the drop on _meta.shellProgress — matching the qwen-agent and web-shell normalizer guards — so real heartbeats are still dropped while malformed frames are flagged. Added a regression test for the malformed path. |
||
|---|---|---|
| .. | ||
| .agents/skills | ||
| .github | ||
| apps | ||
| docs/plans | ||
| packages | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| .nvmrc | ||
| bun.lock | ||
| CODE_OF_CONDUCT.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
| NOTICE | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| TRADEMARK.md | ||
| tsconfig.base.json | ||
| tsconfig.json | ||
Qwen Code
Qwen Code is a desktop and headless agent workspace. It provides multi-session chat, source connections, skills, file previews, automations, and permission modes in a local-first application.
Backend
This fork is Qwen-only:
- Agent sessions run through Qwen Code over ACP.
- The app does not store third-party LLM API keys.
- The built-in LLM connection is
qwen-code. - Legacy multi-provider backends and package/runtime wiring have been removed.
Qwen Code CLI Runtime
The desktop app talks to the Qwen Code CLI over ACP. Treat the CLI as a
runtime artifact, not as desktop source code. A packaged app must bundle a
known CLI build so users can launch it without installing qwen separately.
Use one of these workflows depending on what you are developing:
| Workflow | Use it when | Commands |
|---|---|---|
| Default desktop development | You are developing desktop only. | bun run dev |
| Published npm package | You want a specific published CLI version for dev, CI, or release builds. | QWEN_CODE_VERSION=0.15.12-acp.0 bun run dev |
| Local npm tarball | You need to verify the exact package contents before publishing. | QWEN_CODE_TARBALL=/path/to/qwen-code-0.15.12-acp.0.tgz bun run dev |
| Local qwen-code checkout | You are changing ACP or other CLI behavior while testing desktop. | QWEN_CODE_ROOT=/path/to/qwen-code bun run dev |
| Explicit CLI entry | You need to point at a specific CLI file. | QWEN_CODE_CLI=/path/to/qwen-code/scripts/dev.js bun run dev |
electron:dev uses local overrides first. If no override is set and this
repository is not inside the qwen-code monorepo, it vendors the default version
from qwenCodeRuntime.version in package.json and points Electron at the
vendored CLI automatically.
If you are preparing a package without publishing it, create the tarball from the Qwen Code repository and point desktop at it:
cd /path/to/qwen-code
npm run build
npm run bundle
npm run prepare:package
npm pack
cd /path/to/desktop
QWEN_CODE_TARBALL=/path/to/qwen-code/qwen-code-0.15.12-acp.0.tgz bun run dist:mac
Distribution builds run electron:vendor:qwen automatically. Set
QWEN_CODE_VERSION or QWEN_CODE_TARBALL when you want the packaged app to use
a published or packed CLI artifact. If neither is set, this monorepo builds
from the local checkout; a standalone desktop checkout uses
qwenCodeRuntime.version from package.json.
Development runtime resolution checks sources in this order:
QWEN_CODE_CLI / QWEN_CODE_ROOT / QWEN_CODE_PATH
QWEN_CODE_TARBALL
QWEN_CODE_VERSION
local monorepo checkout
existing vendored CLI
qwenCodeRuntime.version from package.json
Distribution vendoring checks sources in this order:
QWEN_CODE_TARBALL
QWEN_CODE_VERSION
QWEN_CODE_ROOT / QWEN_CODE_PATH
local monorepo checkout
qwenCodeRuntime.version from package.json
Installation
bun install
bun run dev
Common Commands
bun run typecheck:all
bun run test:shared
bun run dev
bun run server:start
Building for Distribution
All build commands run from packages/desktop/.
Prerequisites
- Bun (see
.bun-versionfor exact version) bun install— install all workspace dependencies
Developer Build (no code signing)
Use this for local testing. Produces an ad-hoc signed app.
# macOS (arm64 + x64)
bun run electron:dist:dev:mac
# Windows
bun run electron:dist:dev:win
# Linux
bun run electron:dist:dev:linux
Release Build (with code signing)
bun run electron:dist:mac
bun run electron:dist:win
bun run electron:dist:linux
Release builds require signing credentials via environment variables:
| Variable | Purpose |
|---|---|
CSC_LINK |
Path to signing certificate |
APPLE_ID |
Apple ID for notarization |
APPLE_APP_SPECIFIC_PASSWORD |
App-specific password |
APPLE_TEAM_ID |
Team ID for notarization |
Build Output
All artifacts are written to apps/electron/release/:
| Platform | Artifact |
|---|---|
| macOS | Qwen-Code-Desktop-{arm64,x64}.dmg, Qwen-Code-Desktop-{arm64,x64}.zip |
| Windows | Qwen-Code-Desktop-x64.exe |
| Linux | Qwen-Code-Desktop-x64.AppImage |
What the Build Does
Each electron:dist:* command runs three stages:
electron:vendor:qwen— vendors a Qwen Code CLI runtime intovendor/qwen-code/. SetQWEN_CODE_VERSIONto download a published npm version, orQWEN_CODE_TARBALLto use a localnpm packtarball. If neither is set in this monorepo, it builds from the local checkout.electron:build— compiles the app via esbuild (main + preload), Vite (renderer), and copies resources/assets.electron-builder— downloads the Electron runtime, packages the app, signs it, and produces distributable installers (DMG, NSIS, AppImage).
CLI
bun run apps/cli/src/index.ts run "Hello from Qwen"
bun run apps/cli/src/index.ts run --workspace-dir ./project "Summarize this repo"
The run command spawns a headless server, creates a temporary session, streams the response, and exits. Provider flags are accepted only for compatibility; the backend remains Qwen Code.
Repository Layout
apps/
electron/ Desktop app
cli/ Terminal client
webui/ Web adapter
packages/
shared/ Agent, config, prompts, sessions, sources
server-core/ RPC handlers and session manager
core/ Shared types
ui/ Shared UI components
session-tools-core/
session-mcp-server/
scripts/ Build and packaging helpers
Capabilities
- Multi-session inbox with streaming responses and tool visualization
- Qwen Code model discovery through ACP
- MCP, REST API, and local filesystem sources
- Skills stored per workspace
- Permission modes for planning, asking before edits, and autonomous execution
- File attachments and in-app previews for images, PDFs, Office files, and diffs
- Event-driven automations and messaging integrations
License
Apache 2.0. Third-party dependencies are listed in package manifests and are subject to their respective licenses.