qwen-code/packages/desktop
jinye c538bd70d2
feat(core): emit liveness heartbeats for silent foreground shell commands (#6876)
* 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.
2026-07-15 00:07:26 +00:00
..
.agents/skills feat(desktop): compile macOS 26+ Liquid Glass Assets.car in brand-create (#5284) 2026-06-19 03:57:02 +08:00
.github feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
apps chore(release): desktop desktop-v0.0.5 (#6795) 2026-07-14 05:01:40 +00:00
docs/plans fix(desktop): isolate update feed from CLI releases (#5139) 2026-06-15 15:32:39 +08:00
packages feat(core): emit liveness heartbeats for silent foreground shell commands (#6876) 2026-07-15 00:07:26 +00:00
scripts fix(desktop): isolate update feed from CLI releases (#5139) 2026-06-15 15:32:39 +08:00
.dockerignore feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
.gitignore feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
.nvmrc feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
bun.lock fix(desktop): consolidate path boundary checks (#5545) 2026-06-21 13:05:36 +08:00
CODE_OF_CONDUCT.md feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
CONTRIBUTING.md feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
LICENSE feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
NOTICE feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
package.json chore(release): desktop desktop-v0.0.5 (#6795) 2026-07-14 05:01:40 +00:00
README.md feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
SECURITY.md feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
TRADEMARK.md feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
tsconfig.base.json feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00
tsconfig.json feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) 2026-06-11 21:57:20 +08:00

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-version for 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:

  1. electron:vendor:qwen — vendors a Qwen Code CLI runtime into vendor/qwen-code/. Set QWEN_CODE_VERSION to download a published npm version, or QWEN_CODE_TARBALL to use a local npm pack tarball. If neither is set in this monorepo, it builds from the local checkout.
  2. electron:build — compiles the app via esbuild (main + preload), Vite (renderer), and copies resources/assets.
  3. 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.