chore(desktop): rename packages/desktop-shell to packages/desktop (#12653)

* chore(desktop): rename packages/desktop-shell to packages/desktop

The Electron-era packages/desktop was removed in #9085 and the OpenWork
downstream has forked, so the short name is free and the shell suffix is
the only thing left of the transition. Rename the package directory and
every load-bearing reference: workspace negations (npm + pnpm), the ci.yml
crate filter, desktop-release.yml, the isolation guard, lint/ignore configs,
scripts tests, CLI workspace-fixture tests, and living docs.

User-facing identity is untouched: the crate stays qwen-code-desktop,
productName stays "Qwen Code Desktop", the bundle identifier, updater
feed, release tag scheme, and the legacy electron-bridge asset names are
all unchanged. check-desktop-isolation now guards both path names so the
old one cannot re-enter a workspace set. Dated design docs under
docs/design/ keep their historical paths deliberately.

Refs #8596

* docs(design): follow the rename in the undated notification design notes

The web-shell-browser-notification-details pair is a living doc without a
date prefix, so it falls under the PR's update rule rather than the
historical-record exception.

* docs(desktop): stop citing paths the rename invalidated

Three prose sites and one CI comment still described the pre-rename world,
or contradicted themselves once `packages/desktop` named both the removed
Electron app and the living Tauri shell.

- ci.yml `desktop_shell`: the comment this branch added credited the
  Cargo.toml guard with keeping "branches from either side of the rename"
  green. It does not. A pre-rename head's files sit under
  `packages/desktop-shell`, which the changed-files filter no longer
  matches, so the job skips before the guard ever runs. Say that, and say
  what the guard does cover (a head the filter matched because ci.yml
  itself changed, with no crate at the new path).
- docs/design/web-shell-user-message-links.md: "desktop-shell routing" /
  "desktop-shell clicks" -> "desktop-host", the wording this branch already
  chose in LinkifiedText.tsx for the same rule.
- docs/design/telemetry-runtime-client-attribution-design.md: repoint the
  `runtime.rs` citation. It backs the live `QWEN_CODE_DESKTOP=1` claim, and
  the other document of record for that invariant -- the comment in
  acp-channel-fallback.ts -- was already repointed by this branch, so the
  two disagreed. Only the path token changes; the doc's dated findings are
  untouched.
- brand-create.mjs / SKILL.md / src-tauri/src/main.rs: name the historical
  referent as historical (the Electron `packages/desktop` removed in PR
  9085) instead of letting it read as a self-reference. In main.rs the
  citation `packages/desktop/packages/shared/src/config/storage.ts` now
  looked like a subpath of the file's own package. Rewritten, not deleted:
  the `QWEN_DEFAULT_WORKSPACE_DIR` clause next to it is the only in-code
  documentation of that override and still resolves.

Deliberately not swept: `scripts/check-desktop-isolation.js`'s
`packages/desktop-shell` tripwire, ci.yml's #8132 history comment,
`docs/design/9152-architecture-invariant-classification.md` (names both
spellings and is accurate), and the date-prefixed
`docs/design/2026-07-31-desktop-web-shell-release.md`.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmug1iw422v

* test(scripts): pin the desktop crate path and the isolation guard's coverage

Both invariants fail green today, and this rename is the second time the
crate directory has moved.

`scripts/check-desktop-isolation.js` gained `'packages/desktop'` in
`nativePrefixes` -- the only production-logic line in this branch -- and
nothing read it. Deleting that line leaves the guard's output and exit code
byte-identical, because `isNativeLocation('packages/desktop')` then matches
no remaining prefix; a later PR that drops `!packages/desktop` from the root
workspace list would re-admit the package with CI green.

The `desktop_shell` job names the crate directory in five places (the
changed-files filter, the Cargo.toml guard and its `::notice::` text, the
rust-cache `workspaces:`, and two `working-directory:` values). Nothing
asserted they agree with each other or with a directory that exists, so the
next drift reports a skipped job as a passed one.

The new suite parses both instead of re-running them, and is superset-shaped
on purpose: `nativePrefixes` carries a fourth entry (the
`packages/desktop-shell` tripwire) that nothing negates, and an exact-match
assertion would invite deleting it. The npm/pnpm mirror of the negation list
is already pinned by package-scripts.test.js, so this reads package.json only.

Verified by mutation, each applied alone and then restored: reverting the
filter alternative or either `working-directory` to `packages/desktop-shell`,
dropping `'packages/desktop'` from `nativePrefixes`, and dropping
`!packages/desktop` from package.json all turn this suite red. Today no other
test does.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmug1iw422v

* test(scripts): pin what the desktop-isolation tests actually claim

Four review findings on scripts/tests/desktop-isolation.test.js, each
verified by mutating the thing it claims to pin, before and after.

- The comment above the local `isNativeLocation` copy claimed a change to
  the script's rule "has to be made here too". Nothing reads the rule back
  from the producer -- `nativePrefixesFromSource` parses the array literal
  only -- and narrowing the script's rule to prefix equality leaves the
  suite green. Describe the copy instead of claiming coupling that does not
  exist.
- `still carries the Cargo.toml existence guard` matched only the guard's
  condition text. Flip the body's `changed=false` to `changed=true`, or move
  it after `fi`, and the suite stayed green at 5/5 while the job runs every
  gated step against a crate that is not there. Assert the assignment sits
  inside the guard's own `if ... fi` body.
- Both on-disk checks keyed on the bare directory, so git-ignored residue
  under `packages/desktop-shell` (`node_modules`, which survives a branch
  switch with an empty `git status`) reddened two tests on a tree with no
  defect and blamed the root workspace set. Key on `package.json`, which is
  what `packages/*` requires to make a directory a workspace member; a real
  re-entry still reddens both.
- The `startsWith(filterAlternative)` assertion compared a path built from
  `crateDir` -- `filterAlternative` with one trailing slash stripped -- back
  against `filterAlternative`, so it could not fail for any input. Dropping
  the trailing slash from the changed-files filter in ci.yml left it green.
  Replace it with the property that makes the alternative a path prefix
  rather than a bare string prefix.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmugedurp3j

---------

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
易良andQwen-Coder authored and GitHub committed 2026-09-25 05:41:25 +00:00
1 parent 3541e1f5b0
commit 73aa65a4b4
121 files changed
+402 -204

No files matched your search

+18 -11
View File
@@ -2325,10 +2325,17 @@ jobs:
# classifier makes above. The two-tree `git diff BASE HEAD` this replaced
# is not the PR's diff: it reports everything the base gained since the
# branch point as a change on the PR's side. #8132 added
# packages/desktop-shell, so every branch older than it saw 78 phantom
# desktop-shell paths, ran this job, and failed in a checkout of
# refs/pull/N/head that has no such directory.
- name: 'Detect desktop-shell changes'
# packages/desktop-shell (since renamed to packages/desktop), so every
# branch older than it saw 78 phantom desktop paths, ran this job, and
# failed in a checkout of refs/pull/N/head that has no such directory.
# The rename cuts the other way and is not caught by the guard below: a
# pre-rename head's files sit under packages/desktop-shell, which the
# path filter no longer matches, so the job skips and reports green
# having compiled nothing. Such a branch needs a rebase to be gated
# again. The Cargo.toml guard covers the remaining case -- a head the
# filter matched because ci.yml itself changed, with no crate at the
# new path.
- name: 'Detect desktop changes'
id: 'filter'
env:
GH_TOKEN: '${{ github.token }}'
@@ -2339,7 +2346,7 @@ jobs:
# `previous_filename` too: renaming a file out of the crate changes
# it, and only the old path says so.
if files="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[] | .filename, (.previous_filename // empty)')"; then
if grep -Eq '^(packages/desktop-shell/|\.github/scripts/create-desktop-update-manifest\.mjs|\.github/workflows/ci\.yml|\.github/workflows/desktop-release\.yml)' <<<"${files}"; then
if grep -Eq '^(packages/desktop/|\.github/scripts/create-desktop-update-manifest\.mjs|\.github/workflows/ci\.yml|\.github/workflows/desktop-release\.yml)' <<<"${files}"; then
changed=true
else
changed=false
@@ -2351,12 +2358,12 @@ jobs:
# Whatever the filter decided, a tree without the crate cannot have
# regressed it, and cargo has no manifest to run against — the job
# would report a missing working directory as a failure of the PR.
if [[ "${changed}" == "true" && ! -f packages/desktop-shell/src-tauri/Cargo.toml ]]; then
echo "::notice::packages/desktop-shell/src-tauri is absent from this head; nothing to compile."
if [[ "${changed}" == "true" && ! -f packages/desktop/src-tauri/Cargo.toml ]]; then
echo "::notice::packages/desktop/src-tauri is absent from this head; nothing to compile."
changed=false
fi
echo "changed=${changed}" >> "${GITHUB_OUTPUT}"
echo "desktop-shell changed: ${changed}"
echo "desktop changed: ${changed}"
# cargo test links the Tauri/wry webview, so the WebKit/GTK dev headers
# must be present (mirrors the Linux build job in desktop-release.yml).
@@ -2377,14 +2384,14 @@ jobs:
- uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2
if: "${{ steps.filter.outputs.changed == 'true' }}"
with:
workspaces: 'packages/desktop-shell/src-tauri -> target'
workspaces: 'packages/desktop/src-tauri -> target'
- name: 'Compile and test the desktop crate'
if: "${{ steps.filter.outputs.changed == 'true' }}"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'cargo test --manifest-path src-tauri/Cargo.toml'
- name: 'Run desktop release tests'
if: "${{ steps.filter.outputs.changed == 'true' && runner.os == 'Linux' }}"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'node scripts/test-release.js'
+19 -19
View File
@@ -277,7 +277,7 @@ jobs:
with:
node-version: '${{ env.NODE_VERSION }}'
cache: 'npm'
cache-dependency-path: 'packages/desktop-shell/package-lock.json'
cache-dependency-path: 'packages/desktop/package-lock.json'
- name: 'Configure bundled Node.js cache'
id: 'node-cache-path'
@@ -299,7 +299,7 @@ jobs:
- uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2
with:
workspaces: 'packages/desktop-shell/src-tauri -> target'
workspaces: 'packages/desktop/src-tauri -> target'
- name: 'Install Linux dependencies'
if: "runner.os == 'Linux'"
@@ -320,11 +320,11 @@ jobs:
fi
- name: 'Install desktop dependencies'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'npm ci'
- name: 'Set desktop version'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'node scripts/version.js "${{ needs.prepare.outputs.version }}"'
- name: 'Require updater signing key for publishing'
@@ -444,7 +444,7 @@ jobs:
}
- name: 'Prepare bundled runtime'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'npm run build:runtime'
- uses: 'actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830' # v4
@@ -454,16 +454,16 @@ jobs:
key: 'desktop-node-v2-${{ matrix.rust_target }}-${{ env.NODE_VERSION }}-${{ inputs.dry_run }}'
- name: 'Run desktop tests'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'npm test'
- name: 'Run desktop release tests'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'npm run test:release'
- name: 'Sign bundled vendor binaries (macOS)'
if: "runner.os == 'macOS' && (github.event_name == 'release' || inputs.dry_run == false)"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
shell: 'bash'
env:
APPLE_SIGNING_IDENTITY: '${{ env.APPLE_SIGNING_IDENTITY }}'
@@ -516,15 +516,15 @@ jobs:
- name: 'Refresh bundled runtime checksums after signing (macOS)'
if: "runner.os == 'macOS' && (github.event_name == 'release' || inputs.dry_run == false)"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'node scripts/prepare-runtime.js --refresh-checksums'
- name: 'Verify bundled runtime'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
run: 'npm run smoke:runtime'
- name: 'Build desktop installers'
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
shell: 'bash'
env:
DRY_RUN: "${{ github.event_name == 'release' && 'false' || inputs.dry_run }}"
@@ -546,7 +546,7 @@ jobs:
shell: 'bash'
run: |
set -euo pipefail
app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app' -print -quit)"
app="$(find packages/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app' -print -quit)"
codesign --verify --deep --strict --verbose=2 "$app"
spctl --assess --type execute --verbose=2 "$app"
entitlements="$(mktemp)"
@@ -560,7 +560,7 @@ jobs:
env:
WINDOWS_CONFIG: '${{ env.WINDOWS_CONFIG }}'
run: |
$installer = Get-ChildItem packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe | Select-Object -First 1
$installer = Get-ChildItem packages/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe | Select-Object -First 1
$signature = Get-AuthenticodeSignature $installer.FullName
if ($signature.Status -eq 'Valid') {
Write-Output "Windows installer is Authenticode-signed."
@@ -577,18 +577,18 @@ jobs:
LEGACY_ARCH: '${{ matrix.legacy_arch }}'
run: |
set -euo pipefail
app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name 'Qwen Code Desktop.app' -print -quit)"
app="$(find packages/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name 'Qwen Code Desktop.app' -print -quit)"
if [ -z "$app" ]; then
echo '::error::The legacy-named macOS app bundle was not produced.'
exit 1
fi
destination="packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/electron-bridge"
destination="packages/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/electron-bridge"
mkdir -p "$destination"
ditto -c -k --sequesterRsrc --keepParent "$app" "$destination/Qwen-Code-Desktop-$LEGACY_ARCH.zip"
- name: 'Smoke packaged application'
if: "runner.os == 'macOS'"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
shell: 'bash'
run: |
set -euo pipefail
@@ -599,7 +599,7 @@ jobs:
- name: 'Smoke packaged application'
if: "runner.os == 'Windows'"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
shell: 'pwsh'
run: |
$executable = Get-ChildItem src-tauri/target/${{ matrix.rust_target }}/release/qwen-code-desktop.exe | Select-Object -First 1
@@ -607,7 +607,7 @@ jobs:
- name: 'Smoke packaged application'
if: "runner.os == 'Linux'"
working-directory: 'packages/desktop-shell'
working-directory: 'packages/desktop'
shell: 'bash'
run: |
set -euo pipefail
@@ -621,7 +621,7 @@ jobs:
set -euo pipefail
destination="$RUNNER_TEMP/desktop-artifacts"
mkdir -p "$destination"
bundle_root="packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle"
bundle_root="packages/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle"
while IFS= read -r -d '' artifact; do
name="$(basename "$artifact")"
if [ "$RUNNER_OS" = 'macOS' ]; then
+2 -2
View File
@@ -11,7 +11,7 @@ on:
- '.pnpmfile.mjs'
- 'package.json'
- 'packages/*/package.json'
- '!packages/desktop-shell/package.json'
- '!packages/desktop/package.json'
- '!packages/live-host/package.json'
- '!packages/mobile-shell/package.json'
- 'packages/channels/*/package.json'
@@ -35,7 +35,7 @@ on:
- '.pnpmfile.mjs'
- 'package.json'
- 'packages/*/package.json'
- '!packages/desktop-shell/package.json'
- '!packages/desktop/package.json'
- '!packages/live-host/package.json'
- '!packages/mobile-shell/package.json'
- 'packages/channels/*/package.json'
+1 -1
View File
@@ -33,7 +33,7 @@ packages/cua-driver/
# house style would corrupt the very data the codemod tests assert on.
scripts/codemod/fixtures/
packages/live-host/
packages/desktop-shell/runtime/
packages/desktop/runtime/
packages/core/src/skills/bundled/browser-use/runtime/
# Vendored from mobile-next/mobile-mcp and carries its own eslint config, so
# reformatting it to house style only widens the diff against upstream on the
+1 -1
View File
@@ -97,7 +97,7 @@ Landable.
| `.github/` — workflows, actions, CODEOWNERS | **Report-only** — consumed GitHub-side: triggers, required checks, cross-repo `uses:` |
| `package.json` dependencies | **Report-only** — bundlers and postinstall hide consumers |
| Comments, JSDoc, commented-out code | **Out of scope** — `AGENTS.md` says do not delete existing comments as cleanup |
| `packages/desktop-shell`, `packages/cua-driver`, `packages/mobile-mcp`, `**/generated/**`, `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` | Never a target; always searched as consumers — vitest discovers tests by filename glob, nothing imports them, so an import-based orphan detector matches every live test vacuously |
| `packages/desktop`, `packages/cua-driver`, `packages/mobile-mcp`, `**/generated/**`, `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` | Never a target; always searched as consumers — vitest discovers tests by filename glob, nothing imports them, so an import-based orphan detector matches every live test vacuously |
A settings key with zero read sites is still not cleanup. The key is likely
documented under `docs/users/` and completed from the generated schema;
@@ -121,7 +121,7 @@ Then the gate matching what you touched:
| user-facing strings, `i18n/locales/*` | `npm run check-i18n` |
| `package.json` or dependencies | `npm run check:lockfile` |
| `scripts/` | `npm run test:scripts` — plain `npm test` does not cover it |
| `packages/desktop-shell` | `npm run check:desktop-isolation` |
| `packages/desktop` | `npm run check:desktop-isolation` |
| bundling, the serve fast path, `esbuild.config.js` | `npm run check:serve-fast-path-bundle` |
| CLI behavior | the matching `npm run test:integration:*` script |
@@ -231,7 +231,7 @@ beat the doc.
`--no-ignore`: it surfaces `.qwen/tmp/` scratch copies of this repo, the
same phantom-consumer class as `.claude/worktrees/` — **never name that
one**, every hit there is a phantom consumer.
- `packages/desktop-shell` is negated out of the root `workspaces` list but
- `packages/desktop` is negated out of the root `workspaces` list but
is still shipped code; `packages/mobile-mcp` and `packages/cua-driver` are
vendored. None is a target; all three are consumers.
- Then classify every hit: production / test / snapshot / docs / lint
+2 -2
View File
@@ -108,9 +108,9 @@ hygiene findings.
- Correct: multi-SDK behavior is consistent, protocol fields match the
TS SDK, bridge error mapping preserves the original error class.
- **ui-apps** — the two UI apps (`packages/desktop-shell/` and
- **ui-apps** — the two UI apps (`packages/desktop/` and
`packages/web-shell/`).
- `desktop-shell`: a thin Tauri shell around Web Shell (window
- `desktop`: a thin Tauri shell around Web Shell (window
management, process lifecycle, signing, updates).
- `web-shell`: a client React app (`client/`), a Vite build, and a
daemon proxy; ships as an embeddable component.
@@ -12,7 +12,7 @@
- TS/Python/Java SDK 的主入口(`query()`)直接 spawn CLI(stream-json 模式),自带 `--channel=SDK`
- `--acp` 未显式指定 channel 时回退为 `ACP`(`packages/cli/src/config/config.ts` 的 ACP fallback)
但 `qwen serve`(daemon)的 spawn 工厂启动的是不带 channel 的 `qwen --acp` 子进程(`packages/acp-bridge/src/spawnChannel.ts`),因此**经 daemon 承载的会话——SDK 的 daemon 客户端入口(如 TS SDK 的 `DaemonClient`/`DaemonSessionClient`)、Web Shell、Tauri 桌面 shell——全部上报为 `ACP`**,无法区分。Tauri shell 启动 daemon 时设置了 `QWEN_CODE_DESKTOP=1`(`packages/desktop-shell/src-tauri/src/runtime.rs`),但 telemetry 从未读取该变量。
但 `qwen serve`(daemon)的 spawn 工厂启动的是不带 channel 的 `qwen --acp` 子进程(`packages/acp-bridge/src/spawnChannel.ts`),因此**经 daemon 承载的会话——SDK 的 daemon 客户端入口(如 TS SDK 的 `DaemonClient`/`DaemonSessionClient`)、Web Shell、Tauri 桌面 shell——全部上报为 `ACP`**,无法区分。Tauri shell 启动 daemon 时设置了 `QWEN_CODE_DESKTOP=1`(`packages/desktop/src-tauri/src/runtime.rs`),但 telemetry 从未读取该变量。
`app.channel` 是来自 `~/.qwen/source.json` 的**安装来源**,与入口归因是不同概念,不应被重载。
+2 -2
View File
@@ -61,7 +61,7 @@ the composer is not clickable in the transcript, forcing copy-paste to open it.
| Explicit `https?://` scheme only; no bare `www.`/domains/emails | Minimum surface, near-zero false positives; pasted URLs almost always carry a scheme. |
| ASCII allowlist, not a denylist of prose characters | A denylist is wrong in both directions: unlisted scripts (emoji, Thai) get absorbed into the href, and excluded ASCII quotes truncate URLs containing them (`/wiki/L'Aquila`). The allowlist fails closed to plain text; its boundary behavior is inspired by remark-gfm (which assistant messages use) but not identical to it — known deltas: no left boundary is required before an ASCII alphanumeric (`xhttps://…` still links), a trailing HTML character reference is not resolved (`…/page&amp;` keeps `&amp`), the tokenizer is stricter than GFM on a trailing backtick and backslash (they terminate the match), and a balanced `a[0]` keeps its `]`, and raw `[`, `]`, `{`, `}` stay verbatim in the href where the assistant pipeline's `normalizeUri` percent-encodes them. |
| Raw IRIs are not linkified whole | `https://zh.wikipedia.org/wiki/中文` linkifies only its ASCII prefix. Accepted tradeoff: absorbing CJK would swallow the following prose (`详情见https://example.com即可使用`); percent-encoded URLs are unaffected. Measured wrong-link shapes beyond this: markdown link syntax pasted as plain text (`[https://a.example](https://b.example)`) yields an unusable href, and a bare `%` is percent-encoded on the assistant path (`normalizeUri` in mdast-util-to-hast) — `LinkifiedText` matches that byte-for-byte (`%` not followed by two ASCII alphanumerics → `%25`, e.g. `/100%` → `/100%25`, while `50%off` is kept) while keeping the visible text verbatim. |
| Reuse `isSafeHref` + `useExternalLinkOpener` | Same safety check and desktop-shell routing as assistant-message links; no second policy to maintain. |
| Reuse `isSafeHref` + `useExternalLinkOpener` | Same safety check and desktop-host routing as assistant-message links; no second policy to maintain. |
| Separate tiny util + component instead of routing user text through `Markdown` | User text is intentionally not markdown (composer tags, `white-space: pre-wrap` layout); a regex tokenizer avoids changing that contract. |
| No linkification inside host-provided `renderUserMessageContent` output | That output belongs to the embedding host; overriding it would break the customization contract. |
| Scheduled-task-run prompt is linkified too | Only the header lines are machine-generated; the prompt body is user-authored task instructions, so it gets the same treatment. |
@@ -93,7 +93,7 @@ the composer is not clickable in the transcript, forcing copy-paste to open it.
(every render path: default, annotated segments, host-parser parts,
parse-failure fallback, scheduled-task prompt): URL renders as an anchor
with `target="_blank"` / `rel="noopener noreferrer"`; surrounding text and
composer-tag chips unchanged; desktop-shell clicks route through
composer-tag chips unchanged; desktop-host clicks route through
`useExternalLinkOpener`.
- Playwright `@smoke` spec replaying a user message with a URL through the
mock daemon.
@@ -14,7 +14,7 @@ The body also includes this turn's user prompt, with localized Prompt and Reply
The session-title component retains at most 60 Unicode code points (the application prefix is separate), prompt excerpts at most 80, and reply excerpts at most 120, ending in an ellipsis when truncated. Excerpt cleanup reads at most the first 4096 UTF-16 code units, removes common Markdown formatting and simple inline link destinations, strips control and bidi-formatting characters, and collapses whitespace. Angle-bracket text, including HTML and JSX, is preserved literally because a plain-text notification must not guess whether it is code or markup. Fenced code keeps its content. Truncation avoids splitting Unicode code points; grapheme-cluster truncation is out of scope. No extra configuration or model call is introduced. Browser/OS layout may truncate the combined body further.
Notifications include a package-local copy of the project's existing 128×128 PNG mark from `packages/desktop-shell/src-tauri/icons/128x128.png`, with Vite resolving its URL. The published library inlines the PNG as a data URL so it needs no separately published asset. Business text does not append IP addresses, ports, or navigation URLs. Chrome's site attribution and its own macOS logo are controlled by the browser and OS and cannot be replaced by the business icon. Content may appear in the notification center or on the lock screen; the setting description explains that titles, prompts, and reply excerpts are shown. Shared storage continues to record only deduplication hashes, never bodies, titles, or navigation targets.
Notifications include a package-local copy of the project's existing 128×128 PNG mark from `packages/desktop/src-tauri/icons/128x128.png`, with Vite resolving its URL. The published library inlines the PNG as a data URL so it needs no separately published asset. Business text does not append IP addresses, ports, or navigation URLs. Chrome's site attribution and its own macOS logo are controlled by the browser and OS and cannot be replaced by the business icon. Content may appear in the notification center or on the lock screen; the setting description explains that titles, prompts, and reply excerpts are shown. Shared storage continues to record only deduplication hashes, never bodies, titles, or navigation targets.
## Clicking to open a session
@@ -14,7 +14,7 @@
标题中的会话部分最多保留 60 个 Unicode 码点(应用名称前缀另计),提问摘录最多 80 个,回复摘录最多 120 个,超长时以省略号结尾。摘录清理最多读取前 4096 个 UTF-16 代码单元,移除常见 Markdown 格式、简单内联链接地址、控制字符和双向格式字符,并合并空白。尖括号内容(含 HTML 和 JSX)按字面保留,纯文本通知不猜测它是代码还是标记;代码围栏保留内容。截断不拆开 Unicode 码点,按字素簇截断不在范围内。不增加配置或模型调用。浏览器或系统布局可能进一步截短整体正文。
通知携带项目现有 `packages/desktop-shell/src-tauri/icons/128x128.png` 标志的本包副本,由 Vite 解析图片 URL。发布的库将 PNG 内联为 data URL,无需另行发布图片资源。标题与正文不额外拼接 IP、端口或跳转地址;Chrome 自带的站点来源和 macOS 上的 Chrome 标志由浏览器及系统控制,业务图标不能替换它们。通知内容可能显示在通知中心或锁屏上,设置说明告知会展示标题、提问和回复摘录。共享存储仍仅记录去重哈希,不保存正文、标题或导航目标。
通知携带项目现有 `packages/desktop/src-tauri/icons/128x128.png` 标志的本包副本,由 Vite 解析图片 URL。发布的库将 PNG 内联为 data URL,无需另行发布图片资源。标题与正文不额外拼接 IP、端口或跳转地址;Chrome 自带的站点来源和 macOS 上的 Chrome 标志由浏览器及系统控制,业务图标不能替换它们。通知内容可能显示在通知中心或锁屏上,设置说明告知会展示标题、提问和回复摘录。共享存储仍仅记录去重哈希,不保存正文、标题或导航目标。
## 点击打开会话
+16 -16
View File
@@ -77,22 +77,22 @@ an HTTP daemon. See the
## Repository layout
| Path | Responsibility |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages/cli` | The `qwen` executable, argument parsing, configuration assembly, Ink TUI, headless output, ACP entry point, `qwen serve`, and command-specific adapters. |
| `packages/core` | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. |
| `packages/acp-bridge` | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts. |
| `packages/sdk-typescript` | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`. |
| `packages/web-shell` | The browser UI and daemon React adapter built on the TypeScript SDK. |
| `packages/web-templates` | Web templates packaged as embeddable JavaScript and CSS strings. |
| `packages/audio-capture` | Native microphone capture for voice input. |
| `packages/channels` | The shared channel runtime and platform adapters for messaging services. |
| `packages/desktop-shell`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | Product and editor surfaces that adapt Qwen Code to their host environments. |
| `packages/sdk-java`, `packages/sdk-python` | Language-specific programmatic clients. |
| `packages/cua-driver`, `packages/mobile-mcp` | Computer-use and mobile-device integrations exposed through MCP-compatible boundaries. |
| `integration-tests` | End-to-end coverage for CLI, interactive, SDK, sandbox, hook, and terminal behavior. |
| `docs` and `docs-site` | User, developer, protocol, and design documentation plus the documentation site. |
| `scripts` | Build, packaging, release, validation, and repository-maintenance automation. |
| Path | Responsibility |
| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages/cli` | The `qwen` executable, argument parsing, configuration assembly, Ink TUI, headless output, ACP entry point, `qwen serve`, and command-specific adapters. |
| `packages/core` | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. |
| `packages/acp-bridge` | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts. |
| `packages/sdk-typescript` | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`. |
| `packages/web-shell` | The browser UI and daemon React adapter built on the TypeScript SDK. |
| `packages/web-templates` | Web templates packaged as embeddable JavaScript and CSS strings. |
| `packages/audio-capture` | Native microphone capture for voice input. |
| `packages/channels` | The shared channel runtime and platform adapters for messaging services. |
| `packages/desktop`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension` | Product and editor surfaces that adapt Qwen Code to their host environments. |
| `packages/sdk-java`, `packages/sdk-python` | Language-specific programmatic clients. |
| `packages/cua-driver`, `packages/mobile-mcp` | Computer-use and mobile-device integrations exposed through MCP-compatible boundaries. |
| `integration-tests` | End-to-end coverage for CLI, interactive, SDK, sandbox, hook, and terminal behavior. |
| `docs` and `docs-site` | User, developer, protocol, and design documentation plus the documentation site. |
| `scripts` | Build, packaging, release, validation, and repository-maintenance automation. |
Most code lives in npm workspaces under `packages/`. A package should depend on
another package through its declared public exports rather than through a
+5 -5
View File
@@ -75,9 +75,9 @@ export default tseslint.config(
'docs-site/out/**',
'.qwen/**',
'scripts/codemod/fixtures/**', // codemod test data; intentionally non-idiomatic ink input/output
'packages/desktop-shell/runtime/**',
'packages/desktop/runtime/**',
'packages/core/src/skills/bundled/browser-use/runtime/**',
'packages/desktop-shell/src-tauri/target/**',
'packages/desktop/src-tauri/target/**',
'packages/live-host/**', // standalone Electron app with its own Node test conventions
'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS
'packages/mobile-mcp/**', // vendored mobile-next/mobile-mcp; has own eslint config
@@ -512,8 +512,8 @@ export default tseslint.config(
'docs/**/*.mjs',
// Plan C CDP-tunnel acceptance harness (issue #5626) runs with `node`.
'packages/cli/src/serve/cdp-tunnel/acceptance/**/*.mjs',
// Desktop-shell skill helper scripts also run with `node`.
'packages/desktop-shell/.agents/skills/**/scripts/**/*.mjs',
// Desktop skill helper scripts also run with `node`.
'packages/desktop/.agents/skills/**/scripts/**/*.mjs',
],
languageOptions: {
globals: {
@@ -558,7 +558,7 @@ export default tseslint.config(
},
},
{
files: ['packages/desktop-shell/bootstrap/**/*.js'],
files: ['packages/desktop/bootstrap/**/*.js'],
languageOptions: {
globals: {
...globals.browser,
+1 -1
View File
@@ -22,7 +22,7 @@
"packages/channels/plugin-example",
"integrations/external-context",
"integrations/external-context-mem0",
"!packages/desktop-shell",
"!packages/desktop",
"!packages/live-host",
"!packages/mobile-shell"
],
@@ -2111,7 +2111,7 @@ describe('runBuildTest', () => {
});
it('discloses a diff inside a negated member — softly, never as an incomplete scope', () => {
// packages/desktop-shell is a separate toolchain (its own lockfile); a diff
// packages/desktop is a separate toolchain (its own lockfile); a diff
// inside it cannot fail any npm workspace's suite, so "nothing to run"
// stays the answer — disclosed softly (its own suite did not run), never
// as an incomplete scope.
@@ -2119,7 +2119,7 @@ describe('runBuildTest', () => {
join(root, 'package.json'),
JSON.stringify({
name: 'r',
workspaces: ['packages/*', '!packages/desktop-shell'],
workspaces: ['packages/*', '!packages/desktop'],
scripts: { test: 'exit 0' },
}),
);
@@ -2127,11 +2127,11 @@ describe('runBuildTest', () => {
name: '@x/core',
scripts: { build: 'exit 0', test: 'exit 0' },
});
pkg('packages/desktop-shell', {
pkg('packages/desktop', {
name: '@x/desktop',
scripts: { build: 'exit 0', test: 'exit 0' },
});
writePlan(['packages/desktop-shell/src/main.rs']);
writePlan(['packages/desktop/src/main.rs']);
const rep = runBuildTest({
plan: planPath,
@@ -2143,9 +2143,7 @@ describe('runBuildTest', () => {
expect(rep.build).toEqual([]);
expect(rep.test).toEqual([]);
expect(rep.testScope?.workspaces).toEqual([]);
expect(rep.testScope?.caveat).toContain(
'packages/desktop-shell/src/main.rs',
);
expect(rep.testScope?.caveat).toContain('packages/desktop/src/main.rs');
expect(rep.testScope?.caveat).toContain('were not run');
expect(rep.note).toContain('were not run');
});
@@ -2392,7 +2390,7 @@ describe('runBuildTest', () => {
it('excludes a negated workspace from the build set (integration)', () => {
// `!packages/excluded` must keep that package out — building it could fail on a
// repo where it is a separate toolchain (e.g. packages/desktop-shell, its own lockfile).
// repo where it is a separate toolchain (e.g. packages/desktop, its own lockfile).
writeFileSync(
join(root, 'package.json'),
JSON.stringify({
@@ -56,7 +56,7 @@ describe('classifyPath', () => {
it('recognises generated and vendored files', () => {
const paths = [
'package-lock.json',
'packages/desktop-shell/bun.lock',
'packages/desktop/bun.lock',
'packages/vscode-ide-companion/NOTICES.txt',
'dist/bundle.min.js',
'vendor/lib.go',
@@ -136,18 +136,18 @@ describe('resolveTestScope', () => {
});
it('discloses — softly, not as incompleteness — a member a negation excludes', () => {
// !packages/desktop-shell is a separate toolchain with its own lockfile; a diff
// !packages/desktop is a separate toolchain with its own lockfile; a diff
// inside it cannot fail any included workspace's suite, so it earns no
// incomplete-scope caveat. But its own suite was not run either, and
// "nothing is silent" covers that: a softer line says what did not run.
const scope = resolveTestScope({
changed: ['packages/desktop-shell/src/main.rs'],
globs: ['packages/*', '!packages/desktop-shell'],
changed: ['packages/desktop/src/main.rs'],
globs: ['packages/*', '!packages/desktop'],
packages: PKGS,
skipped: [],
});
expect(scope.workspaces).toEqual([]);
expect(scope.caveat).toContain('packages/desktop-shell/src/main.rs');
expect(scope.caveat).toContain('packages/desktop/src/main.rs');
expect(scope.caveat).toContain('were not run');
expect(scope.caveat).not.toContain('outside every workspace');
});
@@ -196,7 +196,7 @@ export function resolveTestScope(input: {
const unmapped = affected.filter(
(d) => d !== '.' && !scriptsOf.has(d) && !skipped.includes(d),
);
// Files a negation excludes (!packages/desktop-shell — a separate toolchain with
// Files a negation excludes (!packages/desktop — a separate toolchain with
// its own lockfile) cannot affect any included workspace's tests, so they
// earn no incomplete-scope caveat — but their own suites were not run
// either, and "nothing is silent" covers that too: disclose it as the
@@ -35,7 +35,7 @@ const GLOBS = [
'packages/channels/base',
'packages/channels/telegram',
'packages/channels/qqbot',
'!packages/desktop-shell',
'!packages/desktop',
];
describe('workspaceDirFor', () => {
@@ -55,47 +55,36 @@ describe('workspaceDirFor', () => {
});
it('honours a negation, so a separate toolchain is not a member', () => {
// packages/desktop-shell has its own lockfile and is not part of this npm workspace.
// packages/desktop has its own lockfile and is not part of this npm workspace.
// Building it from the root fails.
expect(
workspaceDirFor(
'packages/desktop-shell/apps/electron/src/main.ts',
GLOBS,
),
workspaceDirFor('packages/desktop/apps/electron/src/main.ts', GLOBS),
).toBeNull();
expect(
isWorkspaceMember('packages/desktop-shell/src/a.test.ts', GLOBS),
).toBe(false);
expect(isWorkspaceMember('packages/desktop/src/a.test.ts', GLOBS)).toBe(
false,
);
});
it('re-includes what a negation excluded when a later glob matches again', () => {
// npm's own rule: last match wins, whichever direction it points.
const globs = [
'packages/*',
'!packages/desktop-shell',
'packages/desktop-shell',
];
expect(workspaceDirFor('packages/desktop-shell/src/a.ts', globs)).toBe(
'packages/desktop-shell',
const globs = ['packages/*', '!packages/desktop', 'packages/desktop'];
expect(workspaceDirFor('packages/desktop/src/a.ts', globs)).toBe(
'packages/desktop',
);
});
it('falls back to the surviving OUTER member when a negation excludes a nested one', () => {
// npm keeps packages/desktop-shell in the graph — only src is excluded — and
// npm keeps packages/desktop in the graph — only src is excluded — and
// desktop's test runner collects src/**, so the file is felt by the outer
// member's suite. Declaring it felt by NOTHING would certify "a complete
// answer" over a suite that can fail.
const globs = [
'packages/*',
'packages/desktop-shell/*',
'!packages/desktop-shell/src',
];
expect(workspaceDirFor('packages/desktop-shell/src/x.test.ts', globs)).toBe(
'packages/desktop-shell',
const globs = ['packages/*', 'packages/desktop/*', '!packages/desktop/src'];
expect(workspaceDirFor('packages/desktop/src/x.test.ts', globs)).toBe(
'packages/desktop',
);
expect(isNegationExcluded('packages/desktop/src/x.test.ts', globs)).toBe(
false,
);
expect(
isNegationExcluded('packages/desktop-shell/src/x.test.ts', globs),
).toBe(false);
});
it('treats a ./-prefixed glob like its bare form', () => {
@@ -212,9 +201,9 @@ describe('readRootPackage', () => {
describe('isNegationExcluded', () => {
it('is true when a positive glob claims the file but a negation excludes it', () => {
expect(
isNegationExcluded('packages/desktop-shell/src/main.rs', GLOBS),
).toBe(true);
expect(isNegationExcluded('packages/desktop/src/main.rs', GLOBS)).toBe(
true,
);
});
it('is false for a file inside an included workspace', () => {
@@ -227,27 +216,21 @@ describe('isNegationExcluded', () => {
});
it('is false when a later glob re-includes what the negation excluded', () => {
const globs = [
'packages/*',
'!packages/desktop-shell',
'packages/desktop-shell',
];
expect(isNegationExcluded('packages/desktop-shell/src/a.ts', globs)).toBe(
false,
);
const globs = ['packages/*', '!packages/desktop', 'packages/desktop'];
expect(isNegationExcluded('packages/desktop/src/a.ts', globs)).toBe(false);
});
it('keeps a member owned under a partial negation (`!packages/desktop-shell/*`)', () => {
// npm keeps packages/desktop-shell itself a member — a glob with a subpath
it('keeps a member owned under a partial negation (`!packages/desktop/*`)', () => {
// npm keeps packages/desktop itself a member — a glob with a subpath
// cannot match the dir itself — so a file under it is still owned and its
// suite can feel a change there; it is NOT negation-excluded.
const globs = ['packages/*', '!packages/desktop-shell/*'];
expect(workspaceDirFor('packages/desktop-shell/src/main.ts', globs)).toBe(
'packages/desktop-shell',
const globs = ['packages/*', '!packages/desktop/*'];
expect(workspaceDirFor('packages/desktop/src/main.ts', globs)).toBe(
'packages/desktop',
);
expect(isNegationExcluded('packages/desktop/src/main.ts', globs)).toBe(
false,
);
expect(
isNegationExcluded('packages/desktop-shell/src/main.ts', globs),
).toBe(false);
});
});
@@ -351,9 +334,9 @@ describe('readWorkspacePackages', () => {
});
it('ignores a broken manifest in a NEGATED dir — not a workspace, not our graph', () => {
setup(['packages/*', '!packages/desktop-shell']);
setup(['packages/*', '!packages/desktop']);
write('packages/good', { name: '@x/good' });
write('packages/desktop-shell', '{ not json');
write('packages/desktop', '{ not json');
const { packages, skipped } = readWorkspacePackages(root);
expect(packages.map((p) => p.dir)).toEqual(['packages/good']);
expect(skipped).toEqual([]);
@@ -459,7 +442,7 @@ describe('hasUnmodeledWorkspaceGlob', () => {
it('is false for the shapes the walker models — literals and a trailing /*', () => {
expect(hasUnmodeledWorkspaceGlob(GLOBS)).toBe(false);
expect(hasUnmodeledWorkspaceGlob(['packages/*', 'apps/web'])).toBe(false);
expect(hasUnmodeledWorkspaceGlob(['!packages/desktop-shell'])).toBe(false);
expect(hasUnmodeledWorkspaceGlob(['!packages/desktop'])).toBe(false);
});
it('is true for `**`, an inner `*`, or a `foo-*` prefix the walker cannot model', () => {
@@ -79,10 +79,10 @@ export function workspaceDirFor(
const norm = filePath.replace(/^\.\//, '');
let owner: string | null = null;
// The owners that came before the current one — where a negation falls back
// TO. When a negation excludes a NESTED member (`packages/desktop-shell/*` claimed
// `packages/desktop-shell/src`, then `!packages/desktop-shell/src` excluded it), the
// TO. When a negation excludes a NESTED member (`packages/desktop/*` claimed
// `packages/desktop/src`, then `!packages/desktop/src` excluded it), the
// still-included outer member keeps owning the file: npm keeps
// `packages/desktop-shell` in the graph, and its test runner collects `src/**`.
// `packages/desktop` in the graph, and its test runner collects `src/**`.
// Falling back to the previous owner is what lets that suite feel the
// change instead of the file being declared felt by nothing.
const previous: Array<string | null> = [];
@@ -111,15 +111,15 @@ export function workspaceDirFor(
}
} else if (dir === owner) {
// A negation only excludes the file when it excludes the member that
// currently owns it. `!packages/desktop-shell/*` matches a deeper pseudo-dir
// currently owns it. `!packages/desktop/*` matches a deeper pseudo-dir
// than `packages/*` does, and npm keeps the member itself in the graph
// (a glob with a subpath cannot match a dir with no subpath), so the
// member's suite can still feel a change there. When the negation DOES
// exclude the owner, ownership falls back to the previous, outer member
// — only a negation of THAT one leaves the file owned by nothing. (The
// pop does not re-check the popped owner against negations already
// walked past: a contrived ordering like `!packages/desktop-shell` BEFORE
// `!packages/desktop-shell/src` can resurrect an excluded owner. Realistic
// walked past: a contrived ordering like `!packages/desktop` BEFORE
// `!packages/desktop/src` can resurrect an excluded owner. Realistic
// orderings — the outer negation written last — are exact.)
owner = previous.pop() ?? null;
}
@@ -132,11 +132,11 @@ export function workspaceDirFor(
* every member.
*
* Such a file belongs to a workspace the npm graph does not contain — this
* repo's `!packages/desktop-shell` is a separate toolchain with its own
* repo's `!packages/desktop` is a separate toolchain with its own
* lockfile — so no included workspace's tests can feel a change to it, and it
* must not earn the incomplete-scope caveat a genuinely outside file does. A
* file whose nested member is negated while an OUTER member survives
* (`!packages/desktop-shell/src` under `packages/desktop-shell`) is NOT excluded here:
* (`!packages/desktop/src` under `packages/desktop`) is NOT excluded here:
* `workspaceDirFor` falls back to the outer member, whose suite collects it.
*/
export function isNegationExcluded(
@@ -351,7 +351,7 @@ export function readWorkspacePackages(root: string): WorkspaceGraph {
if (!existsSync(manifest)) continue;
if (workspaceDirFor(`${dir}/package.json`, globs) !== dir) {
// A directory a negation excludes is not a workspace, and its own
// `package.json` says nothing about that — `packages/desktop-shell` is a
// `package.json` says nothing about that — `packages/desktop` is a
// separate toolchain with its own lockfile, and building it from
// here fails. The tell: the POSITIVE globs alone still make the dir its
// own owner, so a negation is what took the ownership away.
@@ -71,7 +71,7 @@ const GLOBS = [
'packages/*',
'packages/channels/base',
'packages/channels/telegram',
'!packages/desktop-shell',
'!packages/desktop',
];
// Skipped on win32, and not for convenience: `mountRootFor` refuses every
@@ -104,30 +104,22 @@ describe('isWorkspaceMember', () => {
});
it('honours a negated glob', () => {
expect(
isWorkspaceMember('packages/desktop-shell/src/a.test.ts', GLOBS),
).toBe(false);
expect(isWorkspaceMember('packages/desktop/src/a.test.ts', GLOBS)).toBe(
false,
);
});
it('honours workspace-glob ORDER — a positive after a negation re-includes', () => {
// npm evaluates the list in order. Filtering all negations first let a
// negation win wherever it sat, which would file a false `unreachable`.
const globs = [
'packages/*',
'!packages/desktop-shell',
'packages/desktop-shell',
];
expect(
isWorkspaceMember('packages/desktop-shell/src/a.test.ts', globs),
).toBe(true);
const reordered = [
'packages/*',
'packages/desktop-shell',
'!packages/desktop-shell',
];
expect(
isWorkspaceMember('packages/desktop-shell/src/a.test.ts', reordered),
).toBe(false);
const globs = ['packages/*', '!packages/desktop', 'packages/desktop'];
expect(isWorkspaceMember('packages/desktop/src/a.test.ts', globs)).toBe(
true,
);
const reordered = ['packages/*', 'packages/desktop', '!packages/desktop'];
expect(isWorkspaceMember('packages/desktop/src/a.test.ts', reordered)).toBe(
false,
);
});
it('does not match a sibling directory by prefix', () => {
@@ -12,7 +12,7 @@
* `cli/src/serve/channel-worker-supervisor.ts`), so a daemon-spawned
* `qwen --acp` child reports `channel=daemon` instead of the generic `ACP`.
* The Tauri desktop shell launches `qwen serve` with `QWEN_CODE_DESKTOP=1`
* (`packages/desktop-shell/src-tauri/src/runtime.rs`) and daemon children
* (`packages/desktop/src-tauri/src/runtime.rs`) and daemon children
* inherit the marker; those sessions report `channel=desktop`, the same
* client identity the Electron desktop passes explicitly.
*
@@ -1,13 +1,13 @@
---
name: desktop-brand-builder
description: Generate a branded Qwen Code desktop package from the Tauri desktop shell using a minimal brandId and logo. Use when the user wants a custom, white-label, or rebranded desktop client, installer, DMG/EXE/AppImage/deb, or one-click brand build on top of packages/desktop-shell.
description: Generate a branded Qwen Code desktop package from the Tauri desktop shell using a minimal brandId and logo. Use when the user wants a custom, white-label, or rebranded desktop client, installer, DMG/EXE/AppImage/deb, or one-click brand build on top of packages/desktop.
---
# Desktop Brand Builder (Tauri shell)
## Goal
Create a branded desktop package from `packages/desktop-shell` with the least
Create a branded desktop package from `packages/desktop` with the least
user input possible. The user should usually provide only:
```text
@@ -20,10 +20,11 @@ website: https://acme.ai
copyright, or updater endpoints unless the user explicitly asks to override
them.
This skill replaces the Electron-era brand builder that lived in the removed
`packages/desktop`. The Tauri shell is the only desktop implementation now;
branding hooks are `src-tauri/tauri.conf.json`, `src-tauri/icons/`, and the
`bootstrap/` startup UI.
This skill replaces the Electron-era brand builder that lived in the older,
since-deleted Electron `packages/desktop` (removed in PR 9085, before this
Tauri shell took over that path). The Tauri shell is the only desktop
implementation now; branding hooks are `src-tauri/tauri.conf.json`,
`src-tauri/icons/`, and the `bootstrap/` startup UI.
## Input Rules
@@ -102,7 +103,7 @@ Create a temporary `brand.json` in the build directory:
}
```
Install dependencies. The brand script itself only needs desktop-shell's
Install dependencies. The brand script itself only needs the desktop package's
own `node_modules`, but `npm run build:runtime` shells out to the repo
root (which uses `cross-env` and other root devDependencies), so the
root install is also required before packaging:
@@ -111,8 +112,8 @@ root install is also required before packaging:
# Root dependencies (needed by build:runtime → cross-env, esbuild, etc.)
npm install
# Desktop-shell dependencies
cd packages/desktop-shell
# Desktop package dependencies
cd packages/desktop
npm install --workspaces=false
cd ../..
```
@@ -121,8 +122,8 @@ Then run this skill's bundled brand creation script with plain Node (the
script has no dependencies beyond Node >= 18):
```bash
node packages/desktop-shell/.agents/skills/desktop-brand-builder/scripts/brand-create.mjs \
--shell-root /absolute/path/to/qwen-code/packages/desktop-shell \
node packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.mjs \
--shell-root /absolute/path/to/qwen-code/packages/desktop \
--config /absolute/path/to/brand.json
```
@@ -149,7 +150,7 @@ What the script does:
Package with the current host target unless the user requested a target:
```bash
cd packages/desktop-shell
cd packages/desktop
npm run build:runtime --workspaces=false
npx tauri build # current platform
```
@@ -169,7 +170,7 @@ npx tauri build --target aarch64-apple-darwin # explicit macOS arm64
For `target: all`, iterate `build:runtime` → `tauri build` per target; run
only targets supported by the current machine or CI environment. Do not
claim cross-platform artifacts were produced unless the files exist.
Artifacts land under `packages/desktop-shell/src-tauri/target/release/bundle/`
Artifacts land under `packages/desktop/src-tauri/target/release/bundle/`
for the host target, or `src-tauri/target/<triple>/release/bundle/` when
`--target <triple>` is used.
@@ -195,7 +196,7 @@ npx @tauri-apps/cli signer generate -w ~/.tauri/my-brand.key
After packaging:
1. Confirm the expected artifact exists under
`packages/desktop-shell/src-tauri/target/release/bundle/`
`packages/desktop/src-tauri/target/release/bundle/`
(or `src-tauri/target/<triple>/release/bundle/` for cross-compile targets)
(`dmg/`, `nsis/`, `appimage/`, or `deb/`).
2. Compute `sha256sum` or `shasum -a 256` for each artifact.
@@ -207,7 +208,7 @@ After packaging:
- Invalid `brandId`: show the regex and ask for a corrected value.
- Missing `logo`: ask for a valid local path.
- Missing bundled script: report that
`packages/desktop-shell/.agents/skills/desktop-brand-builder/scripts/brand-create.mjs`
`packages/desktop/.agents/skills/desktop-brand-builder/scripts/brand-create.mjs`
is missing, and include the expected command.
- Already-branded shell-root: the script refuses to run when
`productName` is no longer the default (`Qwen Code Desktop`). Start from
@@ -2,12 +2,13 @@
/**
* Brand creation script for the Tauri desktop shell.
*
* Patches packages/desktop-shell so a branded desktop app can be built from
* a minimal brand.json. Replaces the Electron-era brand-create.ts that was
* removed together with packages/desktop.
* Patches packages/desktop so a branded desktop app can be built from
* a minimal brand.json. Replaces the Electron-era brand-create.ts, removed
* together with the former Electron packages/desktop (PR 9085); this Tauri
* shell has since taken over that path.
*
* Usage:
* node brand-create.mjs --shell-root /path/to/packages/desktop-shell \
* node brand-create.mjs --shell-root /path/to/packages/desktop \
* --config /path/to/brand.json
*
* Requires Node >= 18. No external dependencies.
@@ -26,7 +27,7 @@ import { extname, join, resolve } from 'node:path';
const BRAND_ID_RE = /^[a-z][a-z0-9-]*$/;
const USAGE =
'Usage: node brand-create.mjs --shell-root /path/to/packages/desktop-shell --config /path/to/brand.json';
'Usage: node brand-create.mjs --shell-root /path/to/packages/desktop --config /path/to/brand.json';
function argValue(name) {
const index = process.argv.indexOf(name);
@@ -43,7 +44,7 @@ function shellRootFromArgs() {
if (!value) fail(USAGE);
const shellRoot = resolve(value);
if (!existsSync(join(shellRoot, 'src-tauri', 'tauri.conf.json'))) {
fail(`desktop-shell package not found: ${shellRoot}`);
fail(`desktop package not found: ${shellRoot}`);
}
return shellRoot;
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 933 B

After

Width:  |  Height:  |  Size: 933 B

@@ -1,11 +1,11 @@
{
"name": "@qwen-code/desktop-shell",
"name": "@qwen-code/desktop",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/desktop-shell",
"name": "@qwen-code/desktop",
"version": "0.0.1",
"devDependencies": {
"@tauri-apps/cli": "^2.8.5"
@@ -1,5 +1,5 @@
{
"name": "@qwen-code/desktop-shell",
"name": "@qwen-code/desktop",
"version": "0.0.1",
"private": true,
"type": "module",
@@ -527,7 +527,7 @@ function testRuntimeNodePtyTargetMapping() {
}
function testRuntimePreparation(directory) {
const testPackageDir = path.join(directory, 'packages', 'desktop-shell');
const testPackageDir = path.join(directory, 'packages', 'desktop');
const testScript = path.join(testPackageDir, 'scripts', 'prepare-runtime.js');
const sourceRoot = path.join(directory, 'source');
const runtimeDir = path.join(testPackageDir, 'runtime');
File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more