74 Commits
Author SHA1 Message Date
a8eb74fdf5 fix(web-shell): polish files and trim preview resources (#14143)
* fix(web-shell): polish files and trim preview resources

* test(cli): observe pending child Hook promises

* fix(web-shell): address Files browser review findings

* fix(vscode): sync Pierre modification notices

* chore: ignore the repo-local .pnpm-store fallback directory

pnpm falls back to a project-local store at .pnpm-store/ when the global
store directory is not writable (e.g. sandboxed CI runners). It is a
regenerable cache that node_modules hard-links into; its thousands of
untracked files otherwise dirty the working tree and fail deterministic
workspace-clean verification.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): address Pierre preview and file display review findings

- getPreviewLanguage folds case on the extension only, so README.MD and
  config.YML highlight while whole-name detector keys (Gemfile, BUILD)
  keep their case-sensitive behavior
- formatFileName strips the full Default_Ignorable_Code_Point class so
  zero-advance Hangul fillers and variation selectors cannot desync the
  tree search projection from the rendered name
- pin the previously unwitnessed pierre-build guards: redirected Shiki
  subpaths count toward the 4 MiB budget, getModuleInfo may return null
  for redirected ids, emitted assets are skipped, and entry-module
  resolveId calls carry no importer
- pin the theme name->module mapping with exact token colors and give
  the formatFileName display sites discriminating coverage with
  repeated-space names

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-16 16:45:56 +08:00
易良 47463b79a7 chore: drop the unanchored workspace/ ignore rule that hides web-shell sources (#13155)
* chore: drop the unanchored workspace/ ignore rule that hides web-shell sources

The bare `workspace/` line under the brand-builder section matches every
directory named workspace at any depth, including the tracked source tree
packages/web-shell/client/daemon/workspace/. Its 34 files are tracked but
ignored, so a new file there never shows in git status and git add -A
skips it silently (a test in #13154 was nearly left out this way).

Nothing in the repo creates a workspace/ directory: the desktop brand
builder writes to brand-builds/, which stays ignored. The line arrived in
a squashed history import (99fd76553e), so its original intent cannot be
recovered from git.

* style(web-shell): format the two daemon/workspace tests prettier never saw

Prettier honours .gitignore, so the old bare `workspace/` rule also kept
packages/web-shell/client/daemon/workspace/ out of the formatting gate.
With the rule gone the gate checks the directory and flags these two
files; this is prettier --write output only.
2026-10-02 03:34:00 +00:00
易良andQwen-Coder 64dd058c9c ci(pnpm): install with pnpm everywhere and retire package-lock.json (#11859)
* ci(pnpm): install dependencies with pnpm in CI and release

CI tests the graph release ships only if both install it the same way, so
this moves both, keeping every `npm run` script and `npm publish` as they are.

- ci.yml (7), release.yml (3) and release-vscode-companion.yml (2) install
  with `corepack pnpm install --frozen-lockfile`; release keeps
  --ignore-scripts plus its explicit postinstall and generate steps.
- setup-node drops the npm cache and sets package-manager-cache: false,
  which setup-node v5+ needs once package.json declares a pnpm
  packageManager.
- A composite action caches the pnpm store on the three always-hosted jobs;
  the ECS pool keeps its store on local disk.
- version.js no longer lets npm reify node_modules: --no-workspaces-update on
  both `npm version` calls, and a package-lock-only refresh in step 9.
- The notices generator, prepare-package's sharp pin and the standalone
  release's native package specs read the installed tree or pnpm-lock.yaml
  instead of package-lock.json keys.
- New size baselines for the three workflows, which were already over them.

* chore(vscode): regenerate NOTICES from the pnpm-installed tree

Generated in CI (pnpm Worktree Smoke on ubuntu-latest) by the ported
generator. Same entry set as before except the five packages npm and pnpm
resolve differently: fdir 6.5.0, picomatch 4.0.5, mdurl 2.1.0,
@types/node 22.20.1 and mime-db 1.52.0 (form-data's pinned copy). No entry
lacks a license text or repository.

* fix(pnpm): resolve undeclared zod peers to the repo's zod, as npm does

The first CI run on this branch failed the export renderer's size budget:
1,930,015 bytes against 1,930,000, where the same commit built on npm's
tree comes to 1,858,186. Comparing both trees' esbuild metafiles and
prebuilt bundles put the whole difference in web-shell's transcript.js
(+100,903 bytes pre-minify), and the added code is zod 4 plus the
@modelcontextprotocol/ext-apps schemas; the sdk daemon bundle is identical.

web-shell bundles ext-apps, whose zod and MCP SDK are peers, and declares
no zod itself, so pnpm filled the peer with the highest locked zod (4.4.3)
while npm serves the hoisted 3.25.76. Declaring zod 3.25.76 as a root
devDependency makes pnpm resolve every such peer to it
(resolve-peers-from-workspace-root is on by default), which is npm's
hoisting behaviour. mobile-mcp keeps its own zod 4.4.3, as under npm; the
npm tree does not change.

* fix(pnpm): keep npm's root @types/node so root-level typechecks match

The no-AK integration gate typechecks integration-tests/, which takes
Node's types from the root node_modules. npm hoists @types/node 20.19.1
there, because acp-bridge and core pin it; pnpm, with no root declaration,
hoisted 22.20.1, whose spawnSync overloads type process-registry.ts's
stdout as string | NonSharedBuffer and fail TS2322. The same code passes on
every npm-installed PR.

Declaring 20.19.1 as a root devDependency puts npm's copy back where a
root-level tsconfig looks. The npm tree does not change.

* ci(pnpm): install with pnpm in the remaining workflows

The follow-up to switching CI and release: every other workflow that
installs the root workspace now uses the pinned pnpm with
--frozen-lockfile, so no job tests a graph that release does not ship.

- e2e, tui-parity, repo-hygiene, web-shell-visuals, serve-ab, sdk-java,
  windows-runner-smoke, qwen-autofix, the SDK releases, finalize-release,
  sync-release-to-oss, the root install in desktop-release and
  cd-cua-driver, and the sandbox Dockerfile.
- setup-node drops the root npm cache (package-manager-cache: false); hosted
  jobs restore the pnpm store through the shared composite action, and
  qwen-autofix keeps af-012's rule that the persistent pool restores no
  remote cache.
- serve-ab and web-shell-visuals install a separate base checkout that can
  predate pnpm-lock.yaml, so those installs fall back to npm ci there.
- Out of scope, unchanged: packages with their own lockfile (cua-driver's
  TypeScript SDK, desktop-shell, live-host, mobile-mcp), and the triage
  lanes with npm-cache.yml and the npm audit gate, which move with the
  package-lock.json retirement.
- New size baselines for the touched workflows; qwen-autofix stays under
  the 470000-byte gate.

* fix(pnpm): link the workspace packages that root-level code imports

npm links every workspace package into the root node_modules, so
integration-tests/ and scripts/, which are not workspace packages
themselves, can import @qwen-code/* by name. pnpm links a workspace
package only where it is a declared dependency, and the root declared
none, so the no-AK integration gate could not find
@qwen-code/qwen-code-core.

The root now declares the seven packages that root-level code imports at
runtime as file: devDependencies: acp-bridge, channel-base, the CLI,
core, sdk, web-shell and web-templates. npm already links them, so its
tree does not change (letting npm re-resolve the lock reproduces the
same root entry); .pnpmfile.mjs rewrites them to workspace:*, which pnpm
links at the root.

* ci(pnpm): move triage, the store producer and the root audit to pnpm

- qwen-triage's verify and tmux-testing lanes install the PR with
  `corepack pnpm install --frozen-lockfile` from a restored pnpm store.
  The one retry clears node_modules first, because pnpm install, unlike
  npm ci, keeps it.
- npm-cache.yml becomes pnpm-store.yml, keyed on pnpm-lock.yaml.
- security-checks audits the root workspace with `pnpm audit`, which reads
  the lockfile, so the root install step goes. The vendored lockfiles keep
  npm, and the retry recognises pnpm's endpoint error too.
- qwen-autofix drops the hosted-only store cache: its heavy jobs forbid
  local `uses: './...'` actions, and the file sits at the size gate.
- cd-cua-driver: setup-node@v4 has no package-manager-cache input.
- package-assets fixtures install sharp rather than listing it in
  package-lock.json, matching how prepare-package now resolves it.
- AGENTS.md, CONTRIBUTING.md and the npm workspace guide install with pnpm.

* feat(review): install pnpm repos in qwen review's build/test

qwen review installed dependencies only for a repo with package-lock.json
and reported a pnpm repo as unsupported, so reviews of qwen-code would
lose their build and test once its npm lockfile goes.

- lockfileInstaller() picks npm or pnpm from the committed lockfile. With
  both, `packageManager: pnpm@...` chooses pnpm; anything else keeps npm.
- A pnpm repo installs with `corepack pnpm install --frozen-lockfile`
  through the same disk, budget and sandbox path as `npm ci`, and counts
  as installed once node_modules/.modules.yaml exists.
- Scripts still run through `npm run`, and the npm path is unchanged.

* ci(pnpm): retire the root package-lock.json

pnpm-lock.yaml is now the only root lockfile.

- check-lockfile keeps the pnpm integrity check and Playwright parity,
  which now reads pnpm importers and snapshots. The npm-vs-pnpm agreement
  and build-approval checks go: pnpm 11's strictDepBuilds and every CI
  install's --frozen-lockfile enforce what they covered.
- pnpm-lock-freshness.yml goes; it regenerated pnpm-lock.yaml from
  package-lock.json.
- Release: version.js drops its package-lock refresh, the release commit
  stages pnpm-lock.yaml and the SDK release stages no lockfile, so neither
  `git add` fails on the missing file. `npm version` in the SDK and
  mobile-mcp releases passes --no-workspaces-update so it cannot reinstall
  the root with npm.
- cd-mobile-mcp installs from the root pnpm lockfile. setup-node in
  cd-mobile-mcp, qwen-code-pr-review and desktop-release no longer keys an
  npm cache on the root lockfile.
- check-desktop-isolation reads pnpm importers; build.js, build_sandbox.js
  and preflight install with pnpm.
- pnpm-lock.yaml, pnpm-workspace.yaml and .pnpmfile.mjs join the autofix
  supply-chain class and repo-hygiene's deny list; pnpm-lock.yaml joins
  autofix's generated-diff exclusions, sdk-java's path filter and the
  platform-sensitivity manifests.
- .gitignore lists /package-lock.json so an npm install cannot commit it
  back. Tests and docs follow.

* fix(standalone): read the node-pty versions from the pnpm lockfile

* chore(vscode-ide-companion): regenerate NOTICES.txt after the lockfile swap

* chore(vscode-ide-companion): regenerate NOTICES.txt from the pnpm dependency tree

The committed NOTICES.txt was generated from an npm install and does not
match the tree produced by the pnpm install the release pipeline now uses
(659 resolved dependencies, differing @types/node and mime-db versions).
Regenerating with the pinned pnpm install makes the freshness check
reproducible.

* fix(ci): drop the npm cache from the daily dependency audit

actions/setup-node's npm cache keys off the root package-lock.json, which this branch retires, so the scheduled job would fail at Set up Node before the audit ran. No pull_request run reaches this workflow to catch it. The root tree is never installed here either: pnpm audit reads pnpm-lock.yaml directly.

Matches the sibling workflows that already set package-manager-cache: false; scripts/tests/security-workflows.test.js now pins it.

* chore(ci): stop banking main-side drift in the workflow size ratchet

The ratchet exists to turn a workflow's growth into one reviewed line. This branch re-recorded entries for 17 workflows it never touches, several upward by 0.4-2.8 KB - drift that happened on main, banked into the manifest without review.

Restore those to their merge-base values, so .size-baseline changes only for the 22 workflows this branch actually edits. The entries were not needed to go green: ci.yml passes the PR's base commit, so a file that drifted on main and is otherwise untouched earns a warning, not a failure.

* fix(release): initialize standalone state before main

* test(browser-use): read Playwright pin from pnpm lock

* fix(ci): address pnpm migration review blockers

* fix(desktop): stop reading the retired root lockfile

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-09-20 02:11:25 +00:00
b7543aeb1b feat(browser-use): add Chrome Native Messaging relay (#11242)
* feat(browser-use): add Playwright browser SDK

* docs(browser-use): document browser use architecture

* fix(browser-use): make browser input actions reliable

* feat(browser-use): preserve screenshot coordinate metadata

* fix(browser-use): recover stale sockets across VM realms

* fix(browser-use): bound locator action retries

* fix(browser-use): keep page input in the background

* fix(browser-use): reject ambiguous input arguments

* fix(browser-use): validate locator composition identity

* fix(browser-use): recover socket locks across VM realms

* fix(browser-use): preserve input results across navigation

* fix(browser-use): capture screenshots through bounded CDP frames

* fix(browser-use): separate input and navigation waits

* feat(browser-use): add Chrome native relay

* fix(chrome-extension): avoid foreground tabs and stale cursor markup

* fix(chrome-extension): require Chrome 125 for flat sessions

* feat(browser-use): add product integration

* test(browser-use): add managed Chrome smoke coverage

* feat(node-repl): expose structured image metadata

* test(browser-use): preserve complete smoke reports

* feat(browser-use): bundle skill and runtime with CLI

* fix(browser-use): install pinned Playwright in fresh workspaces

* feat(browser-use): require explicit skill opt-in

* feat(browser-use): make bundled skill available by default

Keep explicit skill disables authoritative, update documentation and tests, and use the latest Node REPL package in setup instructions.

* fix(ci): build browser-use before Live Host publish

Build the bundled browser runtime before core in the Live Host npm dependency chain, and cover that prerequisite order with a regression test.

* fix(browser-use): output screenshot metadata explicitly

* fix(node-repl): preserve image metadata outside prose budget

* fix(browser-use): address review round on workspace wiring and runtime correctness

- register the workspace in .pnpmfile.mjs, pnpm-lock.yaml, and the
  unit-vitest-configs registry, with the shared-pool timeout ternary and
  off-Linux unhandled-error exemption (fixes the red Install/Test checks)
- map Playwright's 'warning' console type to the contract's 'warn' level
- reject captures whose pixel dimensions contradict the css-pixels
  coordinate contract instead of certifying rescaled images
- keep one unattachable derived tab from failing tabs.list while still
  propagating a lost session
- clear the cached dialog when Chrome resolves it out of band or the page
  navigates, guarded by dialog identity so a newer dialog survives
- synthesize a parseable Browser.getVersion (numeric Chrome major, host
  platform token) so Playwright enables the right keyboard editing commands
- keep YAML-quoted snapshot keys and link props in interactive listings and
  stop claiming subtrees that filtering removed
- always release the mouse button when a drag gesture fails

* fix(dev): build browser-use runtime before startup

* fix(browser-use): isolate cleanup and correct smoke checks

* fix(browser-use): resolve managed Chrome through Playwright

* fix(browser-use): harden native relay and tab lifecycle

* fix(browser-use): drain host output and persist restored tab state

* fix(browser-use): scope dialog handles and clean navigation waiters

* fix(browser-use): preserve input outcomes and release modifiers

* fix(browser-use): drain session teardown and release debugger attachments

* fix(browser-use): correct locator validation and evaluation

* fix(dev): reuse prebuilt Browser Use runtime

* fix(browser-use): address review round on locator, bridge, and screenshot correctness

Co-authored-by: Qwen-Coder <[email protected]>

* fix(browser-use): require installed extension before native host setup

* fix(browser-use): address review round on schema probe bound and regex validation

* fix(browser-use): start transport before Chrome preflight

* fix(browser-use): exclude staged runtime from code checks

* fix(core): declare Browser Use build dependency

* fix(browser-use): preserve successful navigation and dialog input

* fix(browser-use): harden input drain and tab registration cleanup

* fix(browser-use): keep sessions bound to one Chrome profile

* fix(browser-use): bound commands and validate evaluation JSON

* fix(browser-use): reuse unchanged Native Host registration

* fix(browser-use): wait for fresh extension installation

* fix(browser-use): address review round on screenshot origin, dialog scope, and bridge privacy

- screenshot: tolerate settle-probe failure (measure the ratio from the
  captured pixels, re-capture on HiDPI), measure layout after the settle
  wait, and reject screencast frames whose scroll offsets disagree with
  the published origin
- session: clear the cached dialog only on main-frame navigation;
  abort tab attachment when the tab is removed mid-attach
- errors: classify crashes and timeouts by Playwright's structured error
  name instead of page-influenced message text
- locator.press: release chord modifiers when a later token is rejected;
  cap locator.type at the 60k characters its per-character budget can
  deliver inside the 120s ceiling
- snapshot: keep AI-mode [cursor=pointer] generic nodes in the
  interactive listing
- bridge: derive the fallback socket directory as an env-independent
  per-user 0700 subdirectory on all POSIX hosts and verify ownership
  before binding

* fix(browser-use): close review-critical gaps in error mapping, budgets, and downloads

- errors: classify by Playwright error name first; text phrases match only
  the start of the first message line so page-controlled text (log tails,
  quoted markup, page-thrown values) cannot pick the reported code, and a
  TimeoutError name wins over crash-shaped page text
- transport: cdp.send bridge requests default to 130s so operations up to
  the 120s schema ceiling report their own deadline; other requests keep
  the 30s default
- transport: the /tmp fallback socket directory is a per-user leaf directly
  under the sticky root (no shared intermediate a first mover could own);
  ensureSocketDirectory re-verifies the leaf after creation and walks
  ancestors up to the sticky root, requiring each to be owned by this user
  or root and not group/other-writable
- runtime: withModifiers no longer rewrites a completed action into a
  failure when a modifier release fails; chord failure cleanup releases the
  chord's own tokens (including non-modifiers) instead of four fixed names
- playwright: enable CDP download events on tab attach (Playwright's
  noDefaults connect leaves them off, so waitForEvent('download') could
  never resolve); locator.downloadMedia races the caller deadline, aborts
  the in-page fetch at it, resolves media contained in a located wrapper
  before an ancestor link, restricts URL schemes, and names CORS rejections
  distinctly; locator.allTextContents waits for the first match within the
  caller budget and still resolves [] when nothing attaches
- screenshot: full-page captures get a 16 Mi-pixel budget (1920x3000 pages
  now succeed) and the encoded byte ceiling drops to the 4 MiB model image
  limit so captures are not dropped at the model boundary
- snapshot: interactiveOnly promotes only a terminal [cursor=pointer]
  attribute (page text quoting the marker no longer promotes the line), and
  truncation skips over-long lines instead of discarding the rest
- schemas: reject empty string/regex matchers and exact:true combined with
  a regex matcher (Playwright silently drops exact for regexes)
- tests: the SDK evaluate fixture now mirrors production serialization;
  POSIX socket tests carry win32 gates; posix.join keeps the derived socket
  path a pure function of uid and platform on every host

* fix(browser-use): close review criticals in error mapping, chords, downloads, and snapshots

- errors: classify Playwright's genuine strict-mode violation, which
  crosses CDP as the raw exception description and reaches the client
  behind an "Error: " layer ("locator.click: Error: strict mode
  violation: ... resolved to N elements:"); requiring the element count
  keeps a page-thrown lookalike from forging LOCATOR_NOT_UNIQUE
- runtime-helpers: on a failed chord press, release the tokens
  Playwright actually derived ('+' separates only after a non-empty
  token, so a standalone '+' is a literal key) rather than the caller's
  array elements; an element embedding '+' previously released nothing
  and left modifiers held. locator.press uses the same tokenizer
- locator.downloadMedia: resolve an ordered candidate list (element,
  media descendants, anchor descendants, closest ancestor) and keep the
  first candidate that exposes a URL, so a wrapping anchor no longer
  beats the image it contains and a source srcset or an unloaded
  video's source child can supply the URL
- snapshot: unwrap Playwright's single-quoted YAML key before the
  [cursor=pointer] suffix test, so clickable nodes whose accessible
  name forces key quoting stay in the interactive listing
- docs: align the Input completion contract with the shipped modifier
  cleanup semantics (cleanup failures are discarded; only the action's
  own error propagates) in both languages

* fix(browser-use): close round-13 review criticals in errors, srcset, evaluateAll, snapshots, and screenshot origin

* fix(browser-use): point the pnpm importer at a vitest snapshot that exists

Merging main brought in #11625, which regenerated pnpm-lock.yaml and
dropped the [email protected] snapshot keyed with @vitest/browser and
[email protected]. The packages/browser-use importer still referenced
that key, so every Install leg failed with
ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY. Re-key the importer to the snapshot
its sibling packages already use; nothing else in the lockfile changes.

* fix(browser-use): close round-14 criticals in error classification, dialog ordering and screenshot ratio

R13-2: the strict-mode branch of sanitizeOperationError was the only
text-phrase branch not gated on the evaluate channel, so a page-thrown
primitive "strict mode violation: ... resolved to 2 elements:" picked
LOCATOR_NOT_UNIQUE. On that channel only Playwright's "Error: "-wrapped
form classifies now; other channels keep the unwrapped form.

R14-1: Playwright hands a dialog over on a later turn than the bridge
reports its CDP events (a microtask in CRSession plus the in-process
dispatcher's setImmediate), so an open+close pair delivered in one chunk
left a phantom dialog that blocked the tab. Each tab now traces dialog
openings and closes in bridge order and drops a delivered dialog the
bridge already reported closed; a close with no traced opening (a dialog
open before the tab was attached) is never charged to a later dialog, and
the microtask clear stays for the delivered case.

R14-2: window.devicePixelRatio is page script, and a spoofed value became
an unbounded clip.scale while the pixel re-measure only ran when the
probe failed. The probed ratio is now trusted only within 0.25..8, and the
measure-and-recapture correction runs whenever the first fallback capture
disagrees with the requested CSS region, deriving the real ratio from
Chrome's own pixels (measured / (width * scale)).

Also resolve the temp root in the ensureSocketDirectory tests: the
ancestor sweep rejects symlinks, and macOS reaches os.tmpdir() through
/var -> /private/var, so the tests failed locally on macOS.

* fix(browser-use): tear down accepted peers on start failure and pin the transport's untested paths

The esbuild banner declared require/__filename/__dirname/process bindings
the bundle never uses, so any bundled module declaring one of them built
green but failed to load; drop it (and the unused .wasm loader) and import
dist/index.js after bundling so an unloadable bundle fails the build.

When start() fails after listen (chmod, handshake), a peer validated in
between kept server.close() waiting forever; destroy and clear the
accepted sockets first, mirroring stop(). net.Server has no
closeAllConnections(), so that is the whole fix.

Tests now cover the QWEN_BROWSER_USE_SOCKET_PATH override, a hello with a
mismatched protocol version or extension id, onConnectionChange on the
real transport, an in-flight request when the extension disconnects and
the configured request budget, the ok:false error-code mapping,
FrameDecoder split-frame reassembly and its length ceiling, the
start-failure teardown, and all five schema refine predicates.

* fix(browser-use): mirror the contract bounds in the SDK and state the documented limits

nth() now rejects indexes outside the contract's -10000..10000 before
dispatch, and all() fails closed when the plan already has 32 steps or
the match count exceeds what nth() can address, instead of handing back
locators that only fail on first use. Both evaluate script builders
normalise method-shorthand and class-method sources the way Playwright
does (compile-only vm.Script probe, then a function prefix), so those
well-typed callbacks no longer die as a page-side SyntaxError.

providerTab clamps title and url to the claimTab schema's 20000-char cap
so the documented openTabs() -> claimTab() round trip survives long URLs,
and orderOpenTabs() establishes the http(s)-only, newest-first listing
the model documentation promises. downloadMedia derives the file name
from the URL path only for http(s) sources and leaves blob:/data: names
to the browser.

The model-facing documentation now states the 1s read default, that
dom_cua.scroll's x/y are wheel deltas rather than coordinates, and that
a cross-origin resource without CORS headers cannot be downloaded from
the page origin; the design docs record that boundary as follow-up work.
Tests pin every one of these, plus buildLocator's frame, combinator,
filter and positional branches, the file-chooser SDK surface, and the
claimTab argument guards.

* fix(browser-use): keep background tab registration from moving the selection

registerTab selected every tab it registered, and two of its callers are
background paths (derivedTabTracked, syncDerivedTabs via tabs.list), so
a read-only tabs.list could move tabs.selected onto a popup. Selection is
now set only by newTab and claimTab from the info they return.

Also: tabs.get is allowed while a dialog is open, matching tabs.list and
tabs.selected, which already return the dialog-safe TabInfo; the
auxiliary-button click moves Playwright's tracked cursor before sending
the CDP button events so a later wheel lands where the click did; the
dead providerTabId lookup is removed; openTabs applies orderOpenTabs.

Tests: the runtime test file mocks node:timers so vi.useFakeTimers()
actually controls the input drain bound (the 250ms test drops to ~0ms),
and new cases pin the drain clearTimeout, the registration probe's
session detach, promptText pass-through, tabs.get under a dialog, the
openTabs/claimTab fail-closed guards and ordering, popup adoption from
derivedTabTracked, the abandoned navigation waiter's swallowed rejection,
the uploadFiles path guards, and the screencast fallback and byte budget
on the unconstrained capture path.

* test(browser-use): pin the element bindings the runtime gives evaluate scripts

The SDK emits scripts that read the free variables `element` and
`elements`, and the runtime binds them when it compiles the script inside
the page; no test ran either page-side callback, so swapping the binding
name on one side kept the suite green while every locator.evaluate would
throw a ReferenceError in production. Run the real callbacks with fake
elements so the contract is pinned from both ends.

* fix(browser-use): fail closed on page-controlled strict-mode text and prefer contained media over anchor hrefs

* fix(browser-use): bound page title reads and download a located anchor's file over its icon

* fix(browser-use): correct media downloads and frame composition

* fix(browser-use): bound locator.count and allTextContents by the caller deadline

* fix(browser-use): preserve native snapshots for DOM CUA

Co-authored-by: Qwen-Coder <[email protected]>

* fix(browser-use): classify a closed tab from the tab's own state

The pinned playwright-core client renders a closed target as a plain Error:
only TimeoutError sets `name`, so the `name === 'TargetClosedError'` branch
in sanitizeOperationError never fired, and because evaluate-channel text
fails closed by design, a tab that closed or crashed while a page command
ran was reported as OPERATION_FAILED instead of the documented STALE_TAB.

Decide tab-gone in the dispatcher from `tab.stale` and `page.isClosed()`,
which the page observers keep current, before the text classifier runs.
Drop the dead name branch and replace the test that fabricated the name
with one built the way the client builds the error, plus dispatch-level
cases for both signals that fail without the dispatcher check.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(browser-use): restore the kernel realm process binding and land the round-5 review batch

Criticals from review round 5, each reproduced before fixing:

- The bundled skill imports the staged runtime inside the node_repl kernel,
  whose untrusted vm context has no `process` global and denies the
  `node:process` specifier. 3fc2dc3e10 removed the bundle banner that bound
  `process` through createRequire, so the skill's first cell threw
  `ReferenceError: process is not defined`; the build's own load check runs
  in the main realm and could not see it. Restore the binding on the index
  build and add a kernel-backed test that runs SKILL.md's first cell through
  the real kernel and fails on the ReferenceError (R5-4).
- Bump @qwen-code/node-repl-mcp to 0.1.6: this PR changes its published
  source (image metadata channel) and the package is excluded from the
  repo-wide bump, so the release lane's integrity check would fail. Pin the
  browser-use skill to that version, have the skill test assert the pin
  equals the package version, and extend the cua-driver release gate to
  cross-check the browser-use pin (R5-2, R3-5).
- Report an incompatible extension as EXTENSION_VERSION_MISMATCH so
  browser discovery surfaces the update guidance instead of an empty list
  after the connect budget (R5-5).
- Make tab grouping non-fatal in tabs.create, matching its siblings, so a
  cosmetic grouping failure no longer closes the tab just created (R5-6).

Review suggestions verified still applying, with tests proven red under
mutation: typecheck the scripts directory; pin the Native Messaging chunk
boundary and envelope; return installedPaths from the installer; treat a
blank install-home override as unset; warn on a skipped foreign manifest;
refuse a foreign launcher with the refusals' missing red arms; keep the
preflight download waiter handled; settle the parked-profile test; name
the private-directory cause in the bridge start error; declare the finalize
status union once; reject unknown or closed kept tabs; pin the extension's
protocol constants to the SDK source; resolve the extension test root from
the module; drop the duplicated permission assertion; make the group
barrier real; read the overlay style rules; map a vanished tab to STALE_TAB
in the relay; clamp listed tab text to the schema bound; remove the
unreachable onDetach child-session branch; assert the raced tab in the
disconnect test; cover the history scheme filter and session-name reset;
and fix the extension-source and prerequisite wording in the docs.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* test(browser-use): let the SauceDemo smoke recognize equivalent skill usage

The model-driven smoke completed the task correctly on every run but its
transcript checks asserted one exact spelling: the literal runtime entry
path in the model's code, and one parent-scoped price selector with an
exact number of dollar values in the cell's output. A model that builds
the entry from the skill root across cells, scopes the price locator
differently, or echoes its chosen items after the read failed the smoke.

Key the import check on what every spelling shares: the built-in skill
root the CLI substitutes into SKILL.md, the entry file name and the setup
call. Match price reads on the price class, prefer a read that printed
exactly the expected count, take the leading set otherwise, and take the
last such read for the cart since the flow reads it after the inventory.
The smoke-checks test covers all three import spellings, a foreign runtime
and a missing setup call, and the parser's scoped, echoed and non-list
reads. Verified against a real Chromium 151 run: all six checks true.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(browser-use): close six fail-open paths in the playwright adapter

Round-22 review found six Critical defects in the new package; each is
fixed at its root and pinned by a test that goes red without the fix:

- locator.evaluateAll opened a second full timeout window after the
  attach wait had consumed the caller's budget; the wait and the read
  now share one deadline, and a wait that consumed the budget settles
  [] without evaluating.
- tab.goto passed any string to page.goto, so a file:// URL would have
  handed local file contents to the tab's evaluate and snapshot reads;
  goto is gated to http(s) before the navigation runs.
- Synthesized CDP error responses carried no code, so a reply orphaned
  by a target crash tripped Playwright's internal assert and tore down
  the whole transport; they now carry -32001, the orphan-swallow code.
- A dialog already open when a tab attaches is replayed by neither
  Chrome nor Playwright, so the DIALOG_OPEN gate stayed blind while the
  modal blocked the renderer; registration now probes the renderer and
  gates the tab on an unanswered probe, with getJsDialog reporting it
  under a stable handle whose accept/dismiss run over CDP.
- A dialog opening Playwright never delivered stayed in the trace
  forever and swallowed the charge of every later dialog on the tab; a
  closed, undelivered entry is retired once the dispatcher turn has
  passed.
- snapshotRefLocator validated only the ref's shape, so a ref from a
  pre-navigation snapshot could act on whichever element the new
  document happened to hand the same string; refs are bound to the
  snapshot that emitted them and cleared on main-frame navigation.

Co-Authored-By: Qwen-Coder <[email protected]>

* build: exempt the staged Browser Use skill runtime from the dependency-direction check

The browser-use package build stages the bundled skill's runtime, node_modules
included, under packages/core/src/skills/bundled/browser-use/runtime (git-ignored),
and the TUI dependency-direction gate fails on any node_modules inside a protected
root. Declare that staged, ignored root as a build artifact so the gate skips it
without a diagnostic; every other skipped-directory name still fails.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* build: leave the staged Browser Use runtime out of the dependency-direction walk entirely

The staged root is a build artifact, not source: skipping only its
node_modules while still parsing the SDK bundle next to it was inconsistent.
Do not walk anything under a declared staged root; every other node_modules
inside the protected roots remains a diagnostic.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* build(browser-use): stage the skill runtime only for the bundle and for dev mode

The package build used to copy the bundled skill runtime, node_modules
included, into packages/core/src, which every built tree then carried and
which the TUI dependency-direction gate rightly rejects. Follow the qc-helper
convention instead: the bundle step stages the runtime into dist/bundled for
the shipped CLI, and `npm run dev` stages the git-ignored development copy
next to the source SKILL.md, because only dev mode reads bundled skills from
the source tree. No build step writes into src any more, so the gate
exemption added earlier is dropped and the check script returns to main's
version. The kernel-realm test stages its own copy into a temporary skill
directory rather than depending on a build side effect.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* fix(browser-use): deliver bridge messages in arrival order and keep dev info flags fast

Three fixes from the round-3/4 verification and review round 7–8:

- Transport: a response only settles a promise, so its consumer ran on the
  microtask queue while events from the same socket chunk were emitted
  synchronously and overtook it. Playwright installs a page's renderer and
  lifecycle listeners inside `Page.getFrameTree().then(...)`, so an
  overtaking `Runtime.executionContextCreated` found no listener and a
  claimed tab lost its main world for good (B1). Inbound messages now go
  through one per-socket queue whose drain yields a macrotask after
  settling a response, so every continuation runs before the next message;
  chunks arriving meanwhile join the same queue. Regression:
  `delivers a response ahead of the events that share its socket chunk`.
- Relay: `requireControllableTab` judged a popup by `tab.url`, which is
  empty until the navigation commits, while the claim keyed on
  `pendingUrl`; the runtime's immediate `tabs.get` therefore rejected the
  tab the relay had just reported (R8-2). It now judges by
  `pendingUrl || url`, like the claim. Regression:
  `a popup known only by its pending URL stays controllable`.
- dev launcher: the dev-mode runtime staging skips `--version`/`--help`
  (and `-v`/`-h`), restoring the fast path the existing tests pin (R7-1).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

* test(browser-use): make the smoke evidence and the persist-before-connect pin honest

Round 6–7 review findings on the verification tooling:

- The SauceDemo smoke decided "the staged runtime was loaded" from three
  substring tests over all cells joined, so a run whose staged import threw
  and fell back to a foreign entry still passed (R6-1). It now looks at the
  import expressions per cell: a literal path under the staged runtime, or a
  path assembled from the substituted skill root, in a cell that reaches the
  setup export; a literal entry loaded from anywhere else disqualifies the
  run.
- Price reads were attributed to the cart by count and read order, so an
  inventory sorted low-to-high passed as a cart read and the fallback to the
  first read never returned null (R6-2). Reads are now attributed by the
  tab.url() confirmation of cart.html the prompt requires: inventory before
  it, cart after it, and no cart read without it.
- The persist-before-connect invariant was asserted inside the mock's
  postMessage, which the bridge calls under a bare catch, so the reordered
  mutant stayed green (R7-2). The mock now records every hello with the
  identity persisted at that moment and the test asserts the order outside
  the bridge; the mutant is red.
- SKILL.md states the node-repl-mcp 0.1.6 requirement next to the
  emitImage step the agent always reads, with how to detect and re-register
  an older server (R6-3).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MWe8PKNMpsKdQruTWkvFFc

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-09-18 05:50:24 +00:00
顾盼andClaude 829385a14e feat(qwen-live): protocol v7 playback receipts and interactive init — M5 (#10769)
* feat(qwen-live): interactive init command with agent detection

`qwen-live init` walks a new user through setup:

- Scans PATH for supported coding agents (qodercli, qwen, gemini,
  claude, codex) via `--version` probe, resolving each to an absolute
  path. Native agents (qodercli/qwen/gemini) use their resolved binary
  directly; adapter-based agents (claude/codex) configure npx to run
  the ACP adapter, passing the detected binary via env (CODEX_PATH).
- Interactive prompts (prompts library) let the user pick a default
  backend, add additional backends, enter their DashScope API key
  (with env-var reuse option), and set a working directory.
- macOS: checks the Live Host app via LiveHostInstaller.refresh();
  offers to install if missing. Non-macOS: informs the user the Host
  is required.
- Writes ~/.qwen-live/config.json atomically (tmp + rename) with
  user-only permissions (0o600).

Subcommand dispatch in index.ts: `qwen-live init` runs the wizard,
everything else starts the daemon.

* feat(qwen-live,live-host): protocol v7 playback receipts

The injector's playback window no longer guesses from byte counts;
the Host app sends real playback_started/completed events.

Protocol:
- LIVE_HOST_PROTOCOL_VERSION 6 → 7 (types.ts both copies +
  live-host shared/protocol.ts)
- New Host → Daemon messages: host.playback_started, host.playback_completed
  (both carry epoch for call-scoping)
- CLI coordinator copy accepts the new messages (v7 Host connecting to
  built-in serve is not rejected as invalid)
- FakeHost in e2e harness bumped to v7

Daemon side (packages/qwen-live):
- Injector: replaces playbackDeadline (byte ÷ 48kB/s estimation) with
  playbackInProgress boolean + playbackCompletedAt timestamp.
  notePlaybackStarted/notePlaybackCompleted replace noteOutputAudio.
  windowClosedForMs returns -1 while playback is active, waits
  completedAt + quietGapMs after completion, returns 0 if no playback
  or cleared.
- v6 forward compatibility: onOutputAudioDelta calls notePlaybackStarted
  as a fallback (v6 Host never sends receipts; v7 Host does). The
  fallback preserves the existing "speech after audio triggers
  clearOutput" behavior without the byte-estimation wait.
- live-session: onPlaybackStarted/onPlaybackCompleted wired from
  coordinator handlers to injector signals
- Coordinator: parseHostMessage + handleTextFrame dispatch the new
  messages; LiveCallHandlers gains optional onPlaybackStarted/
  onPlaybackCompleted; epoch-scoped (stale receipts ignored)

Host app (packages/live-host):
- audio-engine.ts: constructor gains onPlaybackStarted/
  onPlaybackCompleted callbacks; play() fires started on 0→1 source
  transition; source.onended fires completed on 1→0 transition
  (generation-guarded so clearOutput's stop() doesn't false-fire)
- preload/index.ts: callbacks → IPC live:audio:playback-started/
  playback-completed with the current epoch; play IPC payload changes
  from bare Uint8Array to { audio, epoch }
- main/index.ts: IPC handlers validate epoch === daemon.getEpoch(),
  call daemon.sendPlaybackStarted/sendPlaybackCompleted
- daemon-connection.ts: sendPlaybackStarted/sendPlaybackCompleted
  via sendControl

Tests: injector tests rewritten for receipt-based timing (started
holds, completed + gap delivers); e2e discovery assertion bumped to v7.

* test(qwen-live): v7 parity test and playback receipt coverage

- live-host parity test: version assertion bumped 6→7, 13/13 pass
- injector tests rewritten: playback_started holds window,
  playback_completed + quiet gap delivers, clearOutput reopens
  immediately, speech-started returns playbackInProgress
- e2e: FakeHost protocol version v7, discovery record assertion v7

* docs(qwen-live): rewrite README with quick start, init wizard, v7, and backend table

* fix(live-host): prefix unused IPC event parameters

* fix(live-host): gate playback IPC handlers with isTrustedSender

The playback-started/completed IPC channels skipped the sender
validation every sibling channel applies — an untrusted renderer
could forge playback receipts. Added isTrustedSender(event) as the
first guard, matching the pattern in live:set-output-muted etc.

* fix(qwen-live): wire playback receipts to injector + publish qwen-live with Host release

Two fixes in one commit:

1. Playback receipt wiring (qqqys's blocking review):
   - daemon.ts setHandlers now passes onPlaybackStarted/
     onPlaybackCompleted to the session
   - LiveSession gains notePlaybackStarted/notePlaybackCompleted
     methods that resolve the active context by epoch and forward
     to the injector
   - The per-delta notePlaybackStarted fallback in onOutputAudioDelta
     is removed — it latched playbackInProgress closed with no matching
     completion, blocking all queued injections until the user spoke
   - v7 Host receipts now flow end-to-end: Host audio engine →
     IPC → daemon-connection → WS frame → coordinator → session →
     injector
   - Test updated to call session.notePlaybackStarted directly
     (matching the production coordinator→session path)

2. qwen-live npm publish in live-host-release.yml:
   - Added checkout + install + build + npm publish steps to the
     Host release publish job
   - Version aligned with the Host release version via npm version
   - Published as @qwen-code/qwen-live with --provenance
   - Tag: "preview" for prereleases, "latest" for stable
   - Skips if the version is already published

* fix(live-host-release): omit --provenance on first npm publish

@qwen-code/qwen-live does not exist on npm yet; provenance requires
an existing package. The workflow now checks if the package exists
and omits --provenance on the first publish, adds it on subsequent
ones.

* fix(live-host-release): use single quotes in NPM_TAG expression

GitHub Actions expressions require single quotes for string literals;
double quotes cause actionlint to fail.

---------

Co-authored-by: Claude <[email protected]>
2026-09-02 11:13:04 +00:00
c13aa351a0 feat(serve): hide workspace Browse on headless daemon hosts (#9406)
* fix(serve): let channel workers reach TLS-enabled daemons

The channel worker supervisor always handed workers an http:// loopback
URL, and workers rejected any other scheme, so on a daemon started with
--tls-cert/--tls-key the worker's first capabilities fetch hit the
HTTPS-only listener as plain HTTP and died with "fetch failed" before
reporting ready ("Channel worker exited before ready (code=1)").

- Emit an https:// loopback URL for the worker when TLS is configured
- Accept https loopback in the worker's QWEN_DAEMON_URL validation
- Inject NODE_EXTRA_CA_CERTS with the daemon cert into the worker env
  (merged with an operator-set value, since it accepts a single file)

* fix(serve): make the worker TLS trust injection actually establish trust

Round 1 review found three ways the CA injection this PR adds silently
fails to give channel workers a usable trust anchor (R1-1, R1-2, R1-3),
plus the diagnosability and coverage gaps around it (R1-4..R1-8).

- R1-1: `--tls-cert` was forwarded to the worker verbatim. Workers are
  forked with `cwd: opts.workspace`, so a relative path resolved against
  the worker's cwd instead of the daemon's, Node silently ignored the
  unloadable extra cert, and every handshake failed
  DEPTH_ZERO_SELF_SIGNED_CERT — the exact pre-PR symptom. Resolve once at
  the source, next to the read that already validated it.
- R1-2: the merged CA bundle went to `os.tmpdir()/qwen-worker-ca-<pid>.pem`,
  a path predictable from the daemon PID (CWE-377/CWE-59). A pre-planted
  symlink redirected the write; a pre-planted regular file kept attacker
  ownership and mode while receiving the full cert — the private key too,
  for a combined PEM. Write into an `mkdtempSync` 0700 directory instead,
  the same defence standalone-update.ts already uses in this tmpdir.
- R1-3/R1-4: a serving cert only anchors trust when it signed itself, and
  only reaches the worker when its SANs cover the loopback host workers
  dial. Neither held for the `mkcert` flow this project documents, and
  boot validation checked parse/expiry/validity-window only — so the
  daemon booted green, browsers connected, and every worker restart-looped
  with /health still green. `describeWorkerTlsTrustGaps` names both at
  boot, the way the adjacent expiry guard does. The non-self-signed check
  stays quiet when the operator set NODE_EXTRA_CA_CERTS, since that value
  is merged into the worker bundle and may already carry the issuing root.
- R1-5: the merge-failure `catch` dropped the operator-set
  NODE_EXTRA_CA_CERTS with no diagnostic, and Node stays silent when the
  remaining cert loads fine. Emit a process warning naming both paths.
- R1-6: the bundle was never cleaned up. Merged bundles are now memoized
  per (operator CA, daemon cert) pair — workers respawn on every restart,
  so minting a directory per spawn would leak one per restart — and
  removed on daemon exit.
- R1-7/R1-8: tests for the merge-failure fallback and for the
  `workerTlsCaCertPath` pass-through, plus an end-to-end test that boots
  the daemon with a relative `--tls-cert` and asserts the supervisor gets
  an absolute path and an https daemon URL.

Verification: every fix was mutation-checked — reverting `path.resolve`,
the mkdtemp write, the trust-gap detection, the merge-failure warning, the
group pass-through, and the bundle memoization each turns at least one new
test red. `npx vitest run src/serve/run-qwen-serve.test.ts
src/serve/channel-worker-supervisor.test.ts
src/serve/channel-worker-group.test.ts` → 403 passed. eslint and prettier
clean on the six touched files.

* test(serve): declare the worker TLS trust check's NODE_EXTRA_CA_CERTS reads

The `Test (ubuntu-latest, Node 22.x)` job failed on 04c954dcb9 with a single
red test: `serve process.env guard > allows only documented process-scoped
process.env expressions`. 04c954dcb9 added the worker TLS trust-gap check,
which reads `process.env['NODE_EXTRA_CA_CERTS']` twice in
run-qwen-serve.ts (once to test for it, once to pass it), but did not add
the matching entry to `allowedProcessEnvAccesses`. The guard is an explicit
allowlist, so any undeclared process-scoped read is a failure by design.

Declare `key:NODE_EXTRA_CA_CERTS: 2` and record why this particular read is
process-scoped rather than request-scoped: NODE_EXTRA_CA_CERTS is the trust
store Node already loaded for this process, so the check has to consult the
same value to know whether the operator has already supplied the issuing CA.

Mutation-verified: with the count at 1 instead of 2 the guard test goes red
with the same mismatch shape, so the allowlist is genuinely counting the
occurrences and not just matching the key.

* feat(serve): hide workspace Browse on headless daemon hosts

The Web Shell "Add workspace" dialog shows a Browse button that opens a
native OS directory picker on the daemon host (osascript on macOS,
PowerShell on Windows, zenity on Linux). On headless hosts the picker
can never open — zenity exits with "cannot open display" — so the
button only surfaces a guaranteed error toast on every click.

- Add isNativeDirectoryPickerAvailable: macOS/Windows always pass;
  Linux requires DISPLAY/WAYLAND_DISPLAY plus an executable zenity
  file on PATH (a directory named zenity does not count)
- Advertise a new conditional native_directory_picker serve feature
  only when the probe passes; the bootstrap reduced set omits it like
  its sibling registration tags, which fails closed
- Web Shell passes onPick to the Add workspace dialog only when the
  feature is present, so the button is hidden on headless hosts
- Tolerate the host-conditional tag in the integration capabilities
  snapshot and register the new process.env access with the guard

* fix(serve): judge the worker TLS trust gaps on the whole serving file

R2-1, R2-2, R2-5 from review round 2.

R2-1. `workerDialHost` returned WHATWG `URL.hostname`, which keeps the brackets
on an IPv6 literal (`[::1]`). `isIP('[::1]')` is 0, so `certCoversHost` took the
DNS-name branch and `checkHost('[::1]')` could never match the iPAddress SAN the
certificate actually carries — the boot diagnostic false-positived on every TLS
daemon bound to `::1` with a correct cert, and told the operator to reissue it.
The brackets are now stripped, so the address is checked as an address and also
printed unbracketed the way a SAN spells it.

R2-2. `describeWorkerTlsTrustGaps` built one `X509Certificate` from the file,
which reads only the FIRST PEM block. A standard `fullchain.pem` (leaf +
issuing CA) was therefore judged on its leaf alone and reported as unable to
anchor worker trust — even though the supervisor injects that same whole file
as the workers' `NODE_EXTRA_CA_CERTS`, root included, so trust does establish.
The file is now split into every certificate it carries and the leaf's chain is
walked through them; the gap is reported only when the chain fails to terminate
in a self-signed certificate inside the file. A leaf-only file still reports it.
The walk is bounded by a fingerprint set, so a cross-signed pair cannot loop.

R2-5. The merged-CA-bundle test asserted `toContain('OP-CERT')` +
`toContain('DAEMON-CERT')`, which both survive mutating the join separator to
`''` — with real PEM inputs that mutant fuses `-----END CERTIFICATE-----` onto
the next `-----BEGIN CERTIFICATE-----` and makes the bundle unparseable. It now
asserts the exact bundle text, which pins the separator and the order.

Verified: run-qwen-serve 275/275, channel-worker-supervisor 90/90, eslint and
prettier clean on the touched files. Typecheck error count is 139 both with and
without this change (worktree build skew against the main checkout's stale
`@qwen-code/*` dist; the same 139 appear on the unmodified branch).
Mutation-checked three ways, each reverting exactly one fix:
  - dropping the bracket strip fails both new IPv6 tests
  - `chainIsSelfAnchored` -> `isSelfSignedCert` fails the fullchain test
  - `.join('\n')` -> `.join('')` fails the merged-bundle test

* fix(serve): judge the worker CA bundle by what Node's loader accepts

Round 2 review findings on #9392: 2 Critical, 5 Suggestion.

R2-11 (Critical): the merge treated a merely *readable* operator
NODE_EXTRA_CA_CERTS as trustworthy. Node's certificate loader is
line-strict and all-or-nothing — a bundle built with
`cat a.pem b.pem` where a.pem lacks a trailing newline fuses
`-----END CERTIFICATE----------BEGIN CERTIFICATE-----` onto one line,
and Node then discards the WHOLE bundle, daemon cert included. The
existing fallback only fired on a read *failure*, so this shape sailed
through the success path and left every worker trusting neither the
operator CA nor the daemon cert while /health stayed green. The merge
now extracts blocks with a line-strict PEM matcher and takes the
existing warn-and-fall-back path when the operator file yields no
loadable block or has a marker that produced none.
`tls.createSecureContext({ ca })` does not throw on that shape, so it
is not used as the validator.

R2-12 (Critical): guard the 0o700 bundle-directory mode assertion on
win32. `fs.mkdtempSync` ignores the mode there and libuv synthesises
st_mode from file attributes (0o666 for a writable directory,
structurally never 0o700), so the merge queue's test_windows job would
go red on a test that passes on Linux/macOS. Same guard shape as
observed-contact-store.test.ts.

R2-13: write only certificate blocks into the bundle. A combined
cert+key serving PEM passes boot validation, which parses the first
block alone, so its private key was being copied into a tmpdir file
NODE_EXTRA_CA_CERTS never reads — and that copy outlives a SIGKILLed
daemon, whose `exit` cleanup cannot run.

R2-4: revalidate the merged-bundle cache. It was keyed on paths alone,
so an in-place operator CA rotation never reached respawned workers for
the daemon's whole lifetime (before this PR a respawn read the
operator's file live), and an external tmp cleaner aging out the bundle
directory left every future respawn pointed at a dead path. Cache
entries now carry each source's mtime/size and the bundle's existence
is re-checked on hit.

R2-3: harden the boot-time trust-gap check along the three corners the
review demonstrated, per its stated minimum. Coverage is judged on the
operator CA's *contents* rather than on the variable being set; every
member of the anchor walk has its validity window checked
(`x509.verify` is signature-only and never consults dates, so an
expired root anchored "fine" while every handshake failed
CERT_HAS_EXPIRED); and the leaf-anchor message no longer asserts a
certain failure, since the check cannot see the workers' default trust
store. `chainIsSelfAnchored` becomes `walkWorkerAnchorPath`, which
returns the certificates the walk relied on so the date check can scope
itself to them.

R2-14: pin worker-side acceptance of `https://[::1]:4170`. The formatter
emits it for a `::1` TLS bind and nothing else pinned the `'[::1]'`
entry in LOOPBACK_BINDS, so dropping it as redundant kept every test
green while regressing this PR's own failure mode on IPv6.

R2-6: cover the boot-time warning wiring end to end. Only the pure
function was tested, so deleting the loop, inverting its guard or
feeding it unresolved values all shipped green. Two runQwenServe tests
now boot a real TLS daemon on `::1` (a real SAN gap for a fixture cert
that still pairs with its key) and on 127.0.0.1, asserting the gap text
does and does not reach the daemon log.

BEHAVIOUR FLIP — leaf-anchor gap suppression. A set-but-unhelpful
NODE_EXTRA_CA_CERTS used to silence this warning outright. It no longer
does: a typo'd, unrelated or unloadable path anchors exactly as little
as no CA at all, and suppressing on the variable's mere presence
silenced the diagnostic in the cases it was written for. The test that
pinned the old behaviour is rewritten to assert the new contract rather
than deleted, and three tests cover the paths it used to hide
(anchoring CA, non-anchoring CA, unreadable path).

BEHAVIOUR FLIP — a DER-encoded operator NODE_EXTRA_CA_CERTS is now
refused with a warning instead of concatenated. Node's loader rejects
it either way; the difference is that it no longer takes the daemon
cert down with it.

Verification: packages/cli — run-qwen-serve (283), channel-worker-
supervisor (94), daemon-worker (85), process-env-guard (3),
channel-worker-group — 507 tests pass. eslint and prettier clean.
`tsc --noEmit -p packages/cli` reports 2 errors, both TS6305 against
packages/core/dist; the same 2 appear on the stashed tree, so they are
worktree build skew, not this change. Mutation-verified, 11 of 11
mutants killed: loose PEM regex, whole-file copy (key retained), no
source-stamp revalidation, no bundle stat, `'[::1]'` dropped from
LOOPBACK_BINDS, path-only gap suppression, chain-date check deleted,
unsoftened wording, warn loop gutted, warn guard inverted, wrong
daemonUrl fed to the check. R2-12 is a test-only platform guard with no
production code to mutate.

Co-Authored-By: Claude Opus 5 <[email protected]>

* fix(serve): refuse a non-CA chain terminator and document the worker TLS hop

Clears the four findings still open on #9392 from earlier rounds that
round 2 did not re-report inline.

R2-10: `chainIsSelfAnchored` modelled chain geometry only. OpenSSL also
requires a certificate that SIGNS others to carry
`basicConstraints CA:TRUE`, so a fullchain of leaf + self-signed
CA:FALSE issuer was blessed as anchored while every worker handshake
failed INVALID_PURPOSE — boot green, no warning, the exact silent
outage this diagnostic exists to name. Measured on Node 22 with a real
`tls.connect`: leaf + CA:FALSE self-signed issuer as the trust store →
`INVALID_PURPOSE: unsuitable certificate purpose`.

The constraint binds only PAST the leaf. The same probe shows a
CA:FALSE self-signed cert in its OWN trust store is verified at depth 0
and handshakes fine (`authorized=true`) — which is what plain
`openssl req -x509` produces — so requiring CA:TRUE there would cry
wolf on the ordinary self-signed daemon cert. `walkWorkerAnchorPath`
now rejects a non-CA terminator only when the walk took at least one
step, and reports it separately so the gap text names INVALID_PURPOSE
and the CA:FALSE remedy rather than UNABLE_TO_VERIFY_LEAF_SIGNATURE.
R2-10's other shape — an expired self-signed root — is already covered
by the chain-date check added in the previous commit.

Two fixtures back this: a leaf signed by a self-signed CA:FALSE issuer,
and a self-signed CA:FALSE leaf with loopback SANs. Both were minted
with OpenSSL 3.0.13 and are the exact files the handshake probes above
ran against.

R2-7: no case drove the function to a two-gap outcome, so an inserted
`return gaps` after the first push — or turning the SAN `if` into an
`else if` — survived the whole suite. Under that mutant an operator
fixes the trust anchor, restarts, and only then meets the SAN failure.
Added a CA-issued cert dialled at a host its SANs miss, asserting both
error names.

R2-8: the documented mkcert flow produces a CA-issued leaf — precisely
the shape the new boot warning flags — but the docs never connected
channel workers to TLS (`grep -c NODE_EXTRA_CA_CERTS
docs/users/qwen-serve.md` → 0). Added the HTTPS/TLS note: workers dial
the daemon back over https, self-signed certs and self-carrying
fullchains need nothing, the mkcert flow needs
`NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"` exported in the
daemon's launch environment, and an operator-set value is merged with
the daemon cert rather than replacing it.

R2-9: documented the rotation asymmetry on the `tlsCaCertPath` option,
per the finding's stated minimum. With no operator CA the worker gets
the `--tls-cert` PATH and Node re-reads it at every respawn while the
daemon still serves its boot-time bytes, so an in-place rotation makes
respawned workers restart-loop; with an operator CA the merged bundle
pins a snapshot instead. Either way the rotation needs a daemon
restart, now said in both the JSDoc and the serve docs.

Verification: packages/cli — 510 tests pass across run-qwen-serve
(286), channel-worker-supervisor (94), daemon-worker (85),
process-env-guard (3) and channel-worker-group. eslint clean; prettier
clean including docs/users/qwen-serve.md. `tsc --noEmit -p
packages/cli` reports the same 2 pre-existing TS6305 errors against
packages/core/dist that the stashed tree reports — worktree build skew,
not this change. Mutation-verified, 3 of 3 new mutants killed: CA check
removed, CA check applied to the leaf as well, and the SAN gap
suppressed once a trust-anchor gap exists.

Co-Authored-By: Claude Opus 5 <[email protected]>

* test(serve): guard the exec-bit probe on Windows and pin the Browse… gate

Round 1 review findings on #9406 that belong to this PR's own diff. The
other four Criticals (R1-1..R1-4) and R1-6 were reported against the
stale copy of #9392's channel-worker TLS commit this branch stacks on;
they are cleared by merging #9392's reviewed head, not re-fixed here.

R1-5 (Critical): `rejects a zenity without the executable bit on Linux`
mocks `process.platform` to 'linux' but exercises the real host
filesystem. Windows has no exec bit — libuv's `fs__access` ignores X_OK
entirely, so `fs.accessSync` succeeds for any existing path and the
probe returns true there, failing `.toBe(false)` deterministically.
ci.yml's `test_windows` job is a merge-queue gate that collects the
whole packages/cli suite, so this would block the PR in the queue.
Guarded with `it.skipIf(process.platform === 'win32')`, the shape
packages/core/src/utils/shellContextEnv.test.ts:157 already uses. The
suite stays 21/21 on Linux, so the assertion still executes where the
semantics it asserts exist.

R1-7: nothing observed the `{onPick && (` guard in AddWorkspaceDialog.
App.test.tsx mocks the dialog out entirely so it structurally cannot see
the dialog DOM, and every `browseButton()` call site in
AddWorkspaceDialog.test.tsx sits in a test that passes `onPick`. Turning
that guard into an unconditional render therefore shipped green — and on
a headless daemon host that puts back exactly the dead affordance this
PR removes: a Browse… button whose handler returns immediately. Added
two component tests: mounted without `onPick`, no Browse… button is in
the document; mounted with it, the button is.

Verification: packages/cli native-directory-picker 21 pass,
packages/web-shell AddWorkspaceDialog 36 pass; eslint and prettier
clean. Mutation-verified: `{onPick && (` → `{true && (` turns the new
test red (1 failed / 35 passed). R1-5 has no production code to mutate;
it is a platform guard on a test, verified by the suite still running
green on Linux rather than being skipped there.

Pre-existing and NOT from this change: `server.test.ts > accepts channel
display text only from the workspace worker` fails in this worktree. It
fails identically on c1161a736c, this branch's head before the merge;
this PR's diff does not touch `promptDisplayText` in server.ts; and the
test text is identical on upstream/main. A clean upstream/main baseline
could not be taken in this worktree — the suite refuses to start there
for missing `packages/acp-bridge/dist/sessionMedia.js` build output. CI
reports this PR's checks green.

Co-Authored-By: Claude Opus 5 <[email protected]>

* fix(serve): judge worker CA files with the loader's own rules, on both sides

Round 3 review: 2 Critical (R3-1, R3-2) and 6 Suggestions (R3-3..R3-8).

R3-1 (Critical) — `extractCertificateBlocks` diverged from Node's
NODE_EXTRA_CA_CERTS loader in both directions. Too lax: it validated block
*shape* only, so a body of base64 characters that does not decode was merged
ahead of the daemon cert, and Node then discarded the WHOLE bundle — workers
lost trust in the operator CA *and* the daemon cert while /health stayed
green. Each block is now parsed with `X509Certificate`, the loader's own
parser. Too strict: a UTF-8 BOM, trailing whitespace after a marker line and
leading whitespace on body lines were all rejected into the daemon-cert-only
fallback with a warning that misdiagnosed the file; they are normalised away
before matching.

R3-2 (Critical) — the boot-time trust-gap diagnostic modelled the operator's
CA with a looser parser than the spawn-time merge: a fused-marker bundle, or a
DER file NODE_EXTRA_CA_CERTS never reads, was counted as an anchoring CA at
boot while the merge discarded it and handed workers the daemon cert alone.
The daemon log stayed clean and every worker handshake failed
UNABLE_TO_VERIFY_LEAF_SIGNATURE — the exact silence this diagnostic exists to
end. Both sides now share one extractor, moved to `pem-certificate-blocks.ts`,
and an unloadable operator file is named in a gap instead of being trusted.

R3-8 (behaviour flip) — `X509Certificate.ca` reads false both for an explicit
`basicConstraints CA:FALSE` and for a v1/no-extension root, but OpenSSL
accepts the second as an issuer. The INVALID_PURPOSE boot warning therefore
fired on legacy anchors that work, telling operators to reissue a working CA.
It now fires only when the certificate carries the extension and declares
CA:FALSE. Measured on Node 22 / OpenSSL 3: a leaf anchored by a v1 root
handshakes authorized=true, while the explicit CA:FALSE twin really does fail
INVALID_PURPOSE.

R3-5 — `warnWorkerCaMergeFallback` re-emitted on every spawn, so a
crash-looping worker buried the log stream the operator reads to diagnose it.
Deduped per path pair, keyed on the paths alone so flapping errno text cannot
defeat it.

R3-3 — the `tlsCaCertPath` comment claimed an operator CA pins a snapshot and
makes in-place `--tls-cert` rotation invisible to workers. The code does the
opposite: `resolveWorkerCaCertPath` stamps both sources, so rotation rebuilds
the bundle from the new contents. Corrected to match the code and
docs/users/qwen-serve.md:381.

R3-6 — the probe is right that no test kills
`mergedWorkerCaBundles.delete(cacheKey)`, but no test can: control always
reaches the rebuild, which overwrites the key on success, and every hit
re-stats the bundle and re-compares both stamps before returning it. The
statement could not change an observable result, so it is removed rather than
pinned by a test that would pass without it. The eviction *behaviour* stays
covered by the rotation and tmp-cleaner tests.

R3-4, R3-7 — new coverage: a CRLF operator bundle, a BOM operator bundle, a
marker/body-whitespace bundle, an undecodable block, warn-once-per-pair, and
three boot-log tests that drive the `process.env['NODE_EXTRA_CA_CERTS']` read
and its try/catch end to end through `runQwenServe`.

Every fix was mutation-verified: reverting each one turns exactly its own
test(s) red (9 mutants, 9 kills). The loader claims above were measured
against a real NODE_EXTRA_CA_CERTS handshake on Node 22.23, not inferred.

* fix(serve): judge worker CA framing the way Node's loader does

Round 4 review of #9392: four Critical findings, three of them rooted in
the same place — this code re-implemented Node's `NODE_EXTRA_CA_CERTS`
loader instead of following it.

R4-2 (Critical): `extractCertificateBlocks` pattern-matched what a
well-formed PEM file looks like, and a new divergent shape surfaced in
each of the last three rounds. Replaced with a line scanner that walks
the file the way OpenSSL's `PEM_read_bio_X509` loop does. Three shapes
Node loads and this rejected now extract: a `-----BEGIN CERTIFICATE-----`
substring embedded in a line of prose (markers are matched at line start,
not as unanchored substrings), whitespace inside a base64 body line, and
a UTF-8 BOM in front of a block that is not the first in the file (what
concatenating operator files produces). Every one of them silently fell
back to daemon-cert-only while telling the operator the file "holds no
PEM certificate block Node can load".

BEHAVIOUR FLIP — the loader is prefix-loading, not all-or-nothing. The
doc comment this module carried claimed a malformed block discards the
whole bundle. Measured on Node 22 / OpenSSL 3 through real
`NODE_EXTRA_CA_CERTS` handshakes: a good root followed by a fused block
still handshakes `authorized=true` while Node prints `Ignoring extra
certs … bad end line`. The loader keeps every certificate up to the first
malformed block and loses that block and everything after it. So does
this now; returning `undefined` for the whole file threw away anchors the
workers do in fact receive. The fused-file and bad-decode cases still
return `undefined`, because there the bad block IS the first one.

Both behaviours were taken from the loader, not inferred: 15 shapes were
written to disk, pointed at through `NODE_EXTRA_CA_CERTS` in a child
process, and checked against a real `tls.connect` to a server holding the
leaf they anchor. The parser agrees with the oracle on all 15, and
`pem-certificate-blocks.test.ts` (new — this module had no direct
coverage, which is how three rounds of shapes got through) pins each one
with the measured verdict in the comment.

R4-4 (Critical): `walkWorkerAnchorPath` applied the CA-suitability check
only to the self-signed terminator, so a chain passing THROUGH an
incapable issuer was reported anchored while every worker handshake
failed. Issuer capability is now required of every non-self-signed chain
member the walk leans on. Measured with real handshakes: a CA:FALSE
intermediate and a v3 intermediate with no basicConstraints both fail
INVALID_PURPOSE, and a keyCertSign-only intermediate fails INVALID_CA —
all three reported gaps=NONE before. The self-signed terminator keeps its
existing, looser rule, so the v1 root and CA:FALSE self-signed leaf cases
stay unflagged as measured in earlier rounds.

R4-3 (Critical): the boot diagnostic modelled a merged serving+operator
trust store that the workers never receive when the serving file fails
extraction — `resolveWorkerCaCertPath` finds `daemonBlocks === undefined`,
discards the operator CA and hands them the serving file alone. Boot
reported no gap while every worker handshake failed. The model now
mirrors the fallback and names the discarded operator CA. The comment's
premise (that such a file "cannot serve at all") was false and is gone.

R4-1 (Critical): every `writeMergedWorkerCaBundle` call registered its
own `process.once('exit')` listener. The merge cache is invalidated on
purpose by in-place operator CA rotation and by tmp-cleaner aging, so a
long-lived daemon accumulated a listener, a closure and an orphaned
bundle directory per rebuild, and past the tenth printed
`MaxListenersExceededWarning` into the log stream the fallback dedup
exists to keep readable. One module-level hook now cleans up every minted
directory, and a rebuild removes the directory it supersedes.

R4-5 (Suggestion): the fallback-warning dedup was keyed on the path pair
and add-only, so the first failure silenced every later one. Keyed on a
coarse failure family now, and the keys are lifted when the pair merges
successfully — a changed failure mode and a relapse after a fix are both
new information.

R4-6 (Suggestion): the fallback message blamed markers alone, but this
PR's own X509 decode gate added a third rejection cause. Aligned with the
boot-side wording, which already enumerates all three.

R4-7 (Suggestion): the DER and fused operator-CA tests asserted gap
presence via `.some()` without pinning the count, and never asserted the
DER-specific text. Both now pin `toHaveLength(2)`, and the DER test
asserts its own message.

Every fix is mutation-verified: reverting it turns at least one test red
(9 mutants run, 9 killed).

Verification: `npx vitest run src/serve/pem-certificate-blocks.test.ts
src/serve/channel-worker-supervisor.test.ts
src/serve/run-qwen-serve.test.ts` — 411 passed; channel-worker-group /
-manager / -diagnostics — 84 passed; eslint and prettier clean on the six
touched files. `npm run build` and `npm run typecheck` do not complete in
this worktree for reasons that predate this change and reproduce with it
stashed (a `sharp` typing skew in packages/core and `@qwen-code/*`
resolving to the sibling checkout's dist): 105 typecheck errors with and
without the change, none in the touched files.

* fix(serve): let channel workers reach a concrete bind, and drive the trust
diagnostic off measured OpenSSL behaviour (R2-4, R2-21)

R2-4 — a daemon bound to a concrete interface handed every worker a
`QWEN_DAEMON_URL` the worker's own validator rejected, so the documented
LAN flow passed every boot check and then failed in each worker. The
review's first option (rewrite the URL to loopback) does not hold:
`server.listen(port, hostname)` binds that interface ONLY, and dialling
127.0.0.1 against such a bind is ECONNREFUSED (measured on this host).
The worker therefore keeps dialling the bound address, and the validator
widens from "loopback" to "loopback or an address on this host" — which
keeps the daemon token off the wire exactly as the loopback rule did,
since traffic to an own interface address never leaves the machine. A
bind we cannot certify as local (a DNS name, a literal on no interface)
is now refused at boot by `assertChannelWorkerDaemonUrlIsLocal` instead
of restart-looping every worker with /health green.

R2-21 — the five measured entrances, each closed locally (the structural
rewrite is a separate change):

1. The SAN check and leaf identity now read the loader-framed
   `servingChain[0]`, not the loose parser's `chain[0]`; the two
   disagree when a predecessor's BEGIN marker is absorbed into a
   comment, and the diagnostic judged a certificate never served.
2. A depth-0 server-purpose check on the serving leaf: keyUsage without
   digitalSignature/keyEncipherment/keyAgreement, or an EKU without
   serverAuth, both fail INVALID_PURPOSE (both measured).
3. The issuer-capability check reads extendedKeyUsage too — an EKU on a
   chain member constrains everything below it (measured with a control
   arm: the EKU-free twin authorizes).
4. `pathLenConstraint` is modelled: a pathlen:0 root over an
   intermediate fails PATH_LENGTH_EXCEEDED (measured).
5. The issuer walk prefers a date-valid candidate over an expired
   same-subject twin, killing a false CERT_HAS_EXPIRED alarm against a
   bundle that authorizes (measured).

Suggestions in the same pass: name the issuer's missing keyCertSign
instead of the generic unanchored advice that sends operators to
re-point a CA already provided (measured: checkIssued false, verify
true, handshake "key usage does not include certificate signing"); pin
the PEM scan continuing past a non-certificate block, the operator-
supplied expired chain member, and the v3-no-basicConstraints
intermediate; pin the minted-bundle set's lifetime through a named
`cleanupMintedWorkerCaBundleDirs`; and correct the rotation overclaim in
docs and the `tlsCaCertPath` JSDoc — a fullchain renewed under the same
carried root still verifies (measured), only an anchor that rotates out
breaks the workers.

Every fix is mutation-verified: reverting each one turns its own test
red. Affected suites 516/516 green; the single `tsc -p packages/cli`
error is pre-existing (workspace-service/index.ts, identical before and
after).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

* test(serve): pin the boot wiring, the bracket strip and the pathlen boundary (R5-1, R5-2, R5-5, R5-6, R5-7)

Five round-5 coverage findings, each verified by the mutant the reviewer named.

R5-1 — the boot call to `assertChannelWorkerDaemonUrlIsLocal` was pinned by
nothing: every `runQwenServe` boot test binds loopback, where the assertion
trivially returns, so deleting the call kept the suite green while a bind this
host cannot certify for channels went back to booting green and failing inside
each worker. The new boot test binds this host's own DNS name — the reachable
shape, since the resolver points it at a local interface but
`isOwnInterfaceAddress` refuses to resolve names on the worker startup path —
and asserts `runQwenServe` rejects. Skipped where that name does not resolve to
an address the host holds, the same guard the ::1 bind carries.

R5-2 — the R2-14 comment claimed its test was the sole pin on `'[::1]'` in
`LOOPBACK_BINDS`. That became false when this round's own-interface arm landed:
`validateDaemonWorkerUrl` is now `!isLoopbackBind(…) && !isOwnInterfaceAddress(…)`
and `isOwnInterfaceAddress` strips the brackets and matches `lo`'s `::1`, so the
test survives that deletion. Corrected to say what it actually pins, and to name
the callers the entry still protects (the boot token check, the Host-header
gate), neither of which that suite exercises.

R5-5 — the `V3_NO_BC_INTERMEDIATE` test asserted only `.ca === false` as its
"two predicates disagree" precondition, which the explicit-CA:FALSE fixture also
satisfies. Now pins the property that actually discriminates — the ABSENCE of
the basicConstraints extension in the DER, read the way `declaresNotACa` reads
it — so regenerating the fixture with `basicConstraints=CA:FALSE` can no longer
silently stop covering the shape it is named for.

R5-6 — the pathlen arm was exercised by one violating chain only, leaving the
boundary comparison `depth - 1 <= limit` unpinned: an off-by-one false-alarms on
the routine compliant shape, a `pathlen:0` root signing the serving leaf
directly. New fixture for exactly that chain; measured on Node 22 / OpenSSL 3,
its real handshake authorizes, so the warning would be pure noise. Mutant `<`
for `<=` reddens with the reviewer's message verbatim ("has 0 intermediate CAs
below it").

R5-7 — `local-bind-addresses.ts` had no test file, and every bracketed literal
its two callers see short-circuits on `isLoopbackBind` first, so the whole
normalisation step was unreached and deleting it shipped green. New suite drives
the host's real interfaces rather than a mock: `node:os` is external to the
module graph vitest transforms here, so `vi.mock('node:os')` is visible to the
test file but NOT to the module under test — a mocked version of this suite
passes against a deleted bracket strip. Mutants killed: bracket strip removed,
zone strip removed, `toLowerCase` removed.

Verification: `npx vitest run` over the three touched suites → 407 passed;
`npx eslint` on all four files → clean. Root `npm run typecheck` reports 27
errors both with and without this change (pre-existing worktree skew in
`packages/core` and `web-shell`), so none are from it.

* fix(serve): drive the trust diagnostic off the DER structure, and unbreak the merge queue (R6-2, R2-21)

Round 6's two Critical findings.

R6-2 — `hands workers an absolute --tls-cert path` mints its cert under
`os.tmpdir()` and asserts `path.relative(process.cwd(), certPath)` comes back
relative. On the required merge-queue job `Test (windows-latest, Node 22.x)`
the workspace is on D: and `os.tmpdir()` on C:, where cross-drive
`path.relative` returns the ABSOLUTE target and the precondition throws, so the
PR could not land through the queue. `TMPDIR` cannot move it: win32
`os.tmpdir()` reads TMP/TEMP/USERPROFILE. This branch carried an older copy of
#9392's test; take that PR's fix — fall back to a directory under the vitest
cwd exactly when the relative path comes back absolute — which leaves the Linux
path unchanged.

R2-21 — the boot trust-gap diagnostic models OpenSSL instead of driving it.
The structural rewrite stays scoped to its follow-up PR; this closes the five
entrances round 6 measured, each probe-verified on Node v22.23.0 / OpenSSL
3.0.13 as a real handshake before and after, and each pinned by a test whose
mutant was measured green beforehand.

- S1/K1/D1: `declaresNotACa`, `keyUsageBits` and `pathLenConstraint` located
  their extension with `cert.raw.indexOf(oidBytes)` — an unanchored scan of the
  whole DER, signature and serial included. Both directions measured: a decoy
  extension carrying the basicConstraints OID bytes (DER offset 444, the real
  extension at 482) SWALLOWED a `pathlen:0` gap on a chain that fails
  PATH_LENGTH_EXCEEDED, and a v1 root whose serial is 0x0603551D13 raised a
  CA:FALSE alarm on a chain that authorizes. All three now read one structural
  walk of the TBSCertificate extensions SEQUENCE (`certificateExtension`,
  ported from #9392's `cannotIssueCertificates`), which can match neither.
- EA: `anyExtendedKeyUsage` is not `serverAuth`. The `EKU_ANY` exception in
  `servesTlsClients` and `issuerServesTlsChain` admitted certificates OpenSSL
  refuses for the TLS-server purpose — measured, an anyEKU-only self-signed
  leaf fails INVALID_PURPOSE where its serverAuth twin authorizes, and a
  serverAuth leaf under an anyEKU-only CA fails the same way against the
  fullchain trust store. Both exceptions deleted.
- E1: a keyUsage that is PRESENT and grants no bit was conflated with an ABSENT
  one, which RFC 5280 reads as unrestricted. Measured: a zero-bit leaf fails
  UNABLE_TO_VERIFY_LEAF_SIGNATURE, and a CA:TRUE issuer with a zero-bit
  keyUsage has `checkIssued` false while `verify(publicKey)` is true — the
  `unusableIssuer` shape, which used to draw the generic "point
  NODE_EXTRA_CA_CERTS at the issuing CA" advice for a CA already in the bundle.
- C1: the unloadable-serving-file gap fired only when an operator CA was ALSO
  configured. With none, the workers' NODE_EXTRA_CA_CERTS *is* the serving
  file. Measured through that loader (not the `ca:` option, which reads shapes
  the loader does not): a TRUSTED CERTIFICATE-labelled serving file boots with
  zero gaps while the worker handshake fails DEPTH_ZERO_SELF_SIGNED_CERT and
  the plain-PEM twin authorizes. The gap is now unconditional on
  `!servingBlocks`; the merge-discard clause stays on the operator-CA arm.
- P1: `normalizePemLine` stripped LEADING whitespace before the marker match,
  but Node's loader matches `-----BEGIN/END CERTIFICATE-----` only at column 0.
  Measured: an indented-marker file loads NOTHING (handshake
  DEPTH_ZERO_SELF_SIGNED_CERT, `openssl storeutl -certs` reports
  `Total found: 0`) while the same file un-indented authorizes — so this module
  counted a block the workers never received. Trailing whitespace, CRLF and a
  mid-file BOM stay tolerated, and body-line leading whitespace is unaffected
  because the base64 join drops it, as the decoder does.

Verification: `packages/cli` — run-qwen-serve (320), pem-certificate-blocks
(16) and channel-worker-supervisor (105), 441 passing. Every one of the eight
fixes above was mutation-checked by reverting it in isolation; each turned at
least one test red. The full `src/serve` suite is 5262 passing with the same 3
failures it has on this branch without this change (chmod-based tests that
cannot fail as uid 0). ESLint and Prettier clean on the four touched files;
`tsc --noEmit` reports the same 5 pre-existing errors before and after, none in
these files.

R6-2's guard is a Windows-only branch and is therefore not mutation-checkable
on Linux, where `path.relative` never returns an absolute path.

* fix(serve): dial a wildcard bind's loopback on the family it bound (R7-7)

`assertChannelWorkerDaemonUrlIsLocal` inspected only the URL that
`formatChannelWorkerDaemonUrl` had already rewritten, and that rewrite sent
BOTH wildcard families to `127.0.0.1`. For a `::` / `[::]` bind the guard
therefore constant-folded to `isLoopbackBind('127.0.0.1')` and certified,
vacuously, an address the socket need not answer: an IPv6 wildcard socket
reaches 127.0.0.1 only through a dual-stack mapping the kernel is free to
withhold (`net.ipv6.bindv6only=1`, historically Debian's default and still
set on hardened hosts; IPv6-only hosts). Boot passed, workers got
`QWEN_DAEMON_URL=https://127.0.0.1:<port>`, every dial failed, the first
worker's failure exited the daemon and later channels restart-looped with
`/health` green -- the silent mode the R2-4 guard exists to eliminate.

Measured on this Node, sockets rather than reasoning:

  v6-only [::] socket    dial 127.0.0.1 -> ECONNREFUSED   dial ::1 -> ok
  dual-stack [::] socket dial 127.0.0.1 -> ok             dial ::1 -> ok
  0.0.0.0 socket         dial 127.0.0.1 -> ok             dial ::1 -> ECONNREFUSED

So the mapping has to be per-family, and `::1` is the single address an IPv6
wildcard bind answers on every host. `0.0.0.0` keeps `127.0.0.1` -- the third
row shows over-applying the fix breaks the IPv4 wildcard just as badly.

`''` is grouped with the IPv6 wildcards, not with `0.0.0.0`. The review named
only `::` / `[::]`, but `server.listen(port, '')` -- the exact call this
daemon makes -- reports `{address: '::', family: 'IPv6'}`, so an empty
`--hostname` IS an IPv6 wildcard bind and carries the identical defect.
Leaving it on `127.0.0.1` would have left the entrance open.

BEHAVIOUR FLIP: a TLS daemon on an IPv6 wildcard bind now needs `::1` in its
serving certificate's SANs instead of `127.0.0.1`, and the boot trust
diagnostic will say so. This is the correction, not a side effect -- the old
SAN requirement described a connection the workers were never going to make.
`it.each(['', '0.0.0.0', '::', '[::]'])` pinned the wrong mapping and is
split rather than deleted, so both families stay pinned. Documented in
docs/users/qwen-serve.md; the `--hostname 0.0.0.0` recipe there is unchanged.

Pinned by a live-socket oracle (v6-only, dual-stack, and IPv4 wildcard
servers, dialled at whatever address the function hands a worker) so the
mapping is judged by a real connect, not by a string. Mutation-verified:
reverting the IPv6 arm to 127.0.0.1 -> 4 red (oracle reports ECONNREFUSED);
extending the IPv6 arm to 0.0.0.0 -> oracle red; dropping `''` from the IPv6
arm -> 1 red.

* fix(serve): align worker TLS diagnostics with runtime

* fix(cli): close native picker and TLS review gaps

* fix(cli): close round-12 serve worker-URL and TLS trust-gap findings (#9406)

* test(cli): pin that a home env file CA never reaches channel workers (#9406)

* fix(cli): close round-14 wildcard spelling and TLS trust-gap findings (#9406)

* fix(cli): register the picker env probe with the serve process-env guard (#9406)

* fix(cli): let channel workers accept concrete own-interface daemon URLs (#9406)

The boot guard (`assertChannelWorkerDaemonUrlIsLocal`) certifies concrete
own-interface binds via `isOwnInterfaceAddress`, but the worker-side
`validateDaemonWorkerUrl` still accepted loopback-only URLs — the widening
from 22ca724ee3 was silently dropped by merge 337443c889. Every channel
worker spawned against a certified concrete bind threw
"QWEN_DAEMON_URL must use an http(s) loopback URL." before dialing: the
first worker's failure exited the daemon while /health stayed green.

Restore the widening: accept loopback or an address of one of this host's
own interfaces (traffic to an own-interface address never leaves the
machine, so the daemon token stays on-box exactly as the loopback rule
guaranteed), widen the rejection message, and restore the two worker-side
witness tests so the boot-side and worker-side validators cannot diverge
again.

* fix(cli): refuse zone-scoped channel binds with the named boot diagnostic (#9406)

`qwen serve --hostname 'fe80::…%eth0'` bound fine, then
`assertChannelWorkerDaemonUrlIsLocal` threw a raw `ERR_INVALID_URL`:
`formatHostForUrl` percent-encodes the zone into the worker URL, and
WHATWG URL rejects zone IDs outright — so the failure was visible but
unactionable, instead of the deliberate "Channels cannot start" boot
diagnostic this guard exists to provide.

Catch the parse failure and refuse with the named message: the worker
URL pipeline cannot carry a zone-scoped address even though this host
answers on it. Re-document the `%25` zone decode in `bareAddress` as
defensive-only — production callers feed it from `new URL(...).hostname`,
which never lets a zone survive the parse layer.

* test(cli): let the dial oracle skip loopbacks the host does not assign (#9406)

IPv6-less runners bind `::` yet carry no `::1`, so the certified
loopback dial fails EADDRNOTAVAIL — the arm is unmeasurable there, not
wrong; skip it like a failed bind instead of reddening the suite.

* fix(cli): dial channel workers back on a loopback the host assigns (#9406)

The `::` wildcard mapping handed every channel worker `[::1]`, chosen
from the bind spelling. A host that binds `::` while its loopback
carries no `::1` (e.g. `net.ipv6.conf.lo.disable_ipv6=1`) has only
`127.0.0.1` for the dual-stack socket to answer on, so the first
worker's `fetch failed` exited the daemon — a working `--hostname ::`
+ channels host stopped booting. Derive the dial-back loopback from
the host's interface table instead: `[::1]` when the host assigns it
(an IPv4-less host has no other loopback), `127.0.0.1` otherwise.

The doc bullet and the code comment claimed `net.ipv6.bindv6only=1`
kernels would not answer `127.0.0.1` on an IPv6 wildcard socket.
libuv always sets IPV6_V6ONLY explicitly (to 0 unless `ipv6Only` is
requested), so that sysctl never reaches a Node listener; reword both
to the mechanism that actually applies. The dial oracle's v6-only arm
modelled that same impossible bind — dropped along with the
EADDRNOTAVAIL escape that skipped the one host state the mapping got
wrong; the certified URL is now assigned by construction, so the dial
must succeed wherever the bind does.

Also close the two review-matrix mutations that survived on headless
CI: the boot-time worker URL certification gains a runQwenServe boot
test (an injected certifier records the call and a refusing one fails
the channel boot before workers start), and the capability wiring gets
an injected probe result so the native_directory_picker forwarding is
assertable without a GUI runner.

Verified by mutation: collapsing either mapping branch, making the
loopback probe constant, deleting the boot certification call, and
deleting the capability wiring each fail their focused suites; the
branch shape passes them all.

* fix(cli): certify channel worker URLs only against Host-gate loopbacks (#9406)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): refuse assigned wide-loopback binds the Host gate answers 403 (#9406)

---------

Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: qqqys <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qqqys <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-08-29 00:20:18 +00:00
a511ce4a12 feat(desktop-shell): restore the brand builder skill for the Tauri shell (#10164)
* feat(desktop-shell): restore the brand builder skill for the Tauri shell

The Electron-era desktop-brand-builder skill was removed with packages/desktop in #9085, leaving the Tauri shell without a white-label customization path. Restore it against the new branding hooks: tauri.conf.json, the icon set, and the bootstrap UI. The bundled script is dependency-free Node, regenerates icons via tauri icon, and empties updater endpoints so branded builds never touch the official feed.

* fix(desktop-shell): give brand builder scripts node globals for eslint

The desktop-brand-builder skill script runs under node but lives outside
every existing node-scripts glob in eslint.config.js, so lint:ci failed
with 7 no-undef errors on process/console/URL in brand-create.mjs. Add
the skill scripts path to that existing block.

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

* fix(desktop-shell): complete updater identity patching and fix npx spawn on Windows

- When branded build has no custom updater endpoints, also clear the
  official pubkey and set bundle.createUpdaterArtifacts to false so the
  Tauri bundler does not produce signed updater artifacts that no feed
  will serve.
- Add shell: true to the npx spawnSync call so it works on Windows
  where npx is npx.cmd.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): address review findings for brand builder skill

- Add brand-builds/ to .gitignore (R1-19)
- Validate logo is a file, not a directory (R1-8)
- Document target-specific bundle path in SKILL.md (R1-17)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): harden brand-create against injection and shell safety

- Use function replacers in replaceAll to prevent $& pattern
  interpretation when appName contains dollar signs (R1-3)
- Quote logo path in shell spawn to handle paths with spaces (R1-4)
- Add image extension validation for logo files (suggestion R1-6)
- Document silent config mutations in SKILL.md (suggestion R1-5)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): document cross-compile workflow and guard against re-run

- SKILL.md: document that build:runtime must be re-run with
  QWEN_DESKTOP_TARGET set before each cross-compile target, otherwise
  the packaged artifact contains a wrong-arch Node binary.
- brand-create.mjs: detect already-branded shell-root (productName no
  longer 'Qwen Code Desktop') and fail early with a clear message,
  preventing silent stale results from non-idempotent re-runs.
- SKILL.md: add explicit 'never re-run in the same clone' warning to
  Failure Handling section.

Addresses review findings R1-16 (cross-compile docs) and idempotency
guard from qwen-code-ci-bot.

Co-authored-by: Qwen Code <[email protected]>

* fix(desktop-shell): harden brand-create against updater crash, icon misreport, and JS injection

- Keep updater pubkey as empty string instead of deleting it (the
  tauri-plugin-updater schema requires `pubkey: String` with no default;
  removing it causes deserialization failure at startup).
- Report accurate icon replacement status when `tauri icon` fails on
  non-PNG logos (previously claimed icon.png was replaced even when it
  was not).
- Escape single quotes in appName before injecting into bootstrap.js
  single-quoted string literals (an appName like "Bob's App" caused
  SyntaxError).
- Add root `npm install` step to SKILL.md workflow (build:runtime shells
  out to repo root which needs cross-env and other root devDependencies).

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): eliminate shell injection in brand icon generation (R1-4)

Replace shell:true npx spawn with direct Node invocation of the Tauri
CLI entry point. The logo path is now passed as a plain argv element,
preventing command substitution via crafted filenames ($(cmd), `cmd`).

Falls back to shell-less npx when @tauri-apps/cli is not resolvable
from the shell root.

Adds a test verifying generateIcons contains no shell:true usage.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): require updaterPubkey when custom updater feed is configured (R1-1)

When a brand supplies updaterEndpoints, the matching signing pubkey must
also be provided. Without this check, custom feeds were paired with the
official pubkey, breaking update verification permanently.

loadConfig now fails fast when updaterEndpoints is non-empty but
updaterPubkey is missing. patchTauriConfig sets the pubkey from the
brand config when endpoints are present.

SKILL.md documents updaterPubkey and TAURI_SIGNING_PRIVATE_KEY usage.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): reject appName matching pristine default to close re-run guard bypass (R3-4)

When appName equals 'Qwen Code Desktop' (either explicitly or derived
from brandId like 'qwen-code-desktop'), the detectAlreadyBranded guard
never fires on the first run, allowing a second run to double-patch
bootstrap strings.

loadConfig now rejects this appName at validation time, before any
files are mutated. Covers both explicit override and derived paths.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): silence empty-block lint in brand-create-safety test

Add /* best-effort cleanup */ comments inside two catch blocks used for
teardown so ESLint no-empty rule is satisfied.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): escape backslashes and newlines in appName for bootstrap.js injection

brand-create.mjs only escaped single quotes before splicing appName into the single-quoted string literals of bootstrap/bootstrap.js. A free-form appName ending in a backslash (or containing a newline) produced syntactically invalid generated bootstrap.js while the script still exited 0 reporting bootstrapPatched. Derive the injected content via JSON.stringify so backslashes, newlines, and control characters are all escaped, then re-quote it for the single-quoted context. Add a safety test covering hostile appName values.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(desktop-shell): HTML-escape appName in bootstrap/index.html, fail closed on missing updater section (R5-1, R5-8)

R5-1: appName was spliced raw into bootstrap/index.html (<title>, alt
attribute, <h1>, <h2>), so free-form brand names with <, > or " produced
broken markup (and a </title> breakout). The index.html path now uses an
HTML-escaped form while bootstrap.js keeps its JS-literal escaping.

R5-8: when the target shell-root's tauri.conf.json has no plugins.updater
section, the brand's validated updaterEndpoints/updaterPubkey were silently
discarded while brand-create exited 0 reporting them as applied. Fail closed
before any file is written, leaving the config unmutated.

Tests: hostile-appName index.html fixture assertions (escaped output, no raw
breakout) and a no-plugins.updater shell-root case (non-zero exit, stderr
names the missing section, config byte-identical). Both new assertions go red
when the corresponding fix is removed. Suite 8/8 green; prettier + eslint
clean on both changed files.

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

* fix(desktop-shell): fail on non-array updaterEndpoints in brand.json

A scalar updaterEndpoints (e.g. a string typo of the JSON-array shape documented in SKILL.md) was silently coerced to [] before the updater-safety guards ran, so brand-create exited 0 while shipping the brand with in-app updates permanently disabled. Reject any non-array value with an explicit error naming the expected shape; omitted or [] still disables updates as documented.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen Code <[email protected]>
Co-authored-by: root <[email protected]>
2026-08-27 12:47:50 +00:00
易良 fb6637f0d3 chore(ci): Add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and Scorecard (#9008)
* chore(ci): add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and scorecard workflows

* chore(ci): pin TruffleHog scanner version and drop invalid path input

* fix(ci): close security workflow review gaps

* fix(ci): fail package audit on install errors

* test(ci): pin security workflow guardrails

* fix(ci): pin security workflow test assertions for SHA refs, status edges, and push trigger

* test(ci): pin security workflow edge guards

* test(ci): pin security workflow contracts

* test(ci): pin secret-scan push guard

* fix(ci): quote secret-scan condition

* fix(ci): audit workspace package locks directly

* fix(ci): scope security checks concurrency

* docs(ci): explain mobile audit skip

* test(ci): link trufflehog version pin
2026-08-14 01:22:53 +00:00
Shaojin WenandQwen-Coder 7bc71e4207 feat(review): add qwen-code repository context manifest (#8654)
* feat(review): add qwen-code repository context manifest

Co-authored-by: Qwen-Coder <[email protected]>

* fix(review): keep repo context manifest under relatedPaths budget

Drop the review rule's relatedPaths glob, which resolved ~125 files and
left the merged expansion four files of headroom under the 128 cap: any
co-matching rule (core/skills, core/config, web-shell) pushed the sum
past the cap and aborted `review repo-context` fail-closed before any
agent launched. Changed files are already in the diff, and the rule's
domains, tests, and configurations still attach.

Add a collocated test that runs the real provider against the committed
manifest — per-rule matching, web-shell expansion, and the widest
co-matching combination — so a future hand-edit the strict parser
rejects, or that outgrows the budget, fails in CI instead of mid-review.

* fix(review): exclude local build artifacts from review context expansion

The web-shell e2e relatedPaths glob expanded against the live filesystem,
so developer-local Playwright artifact trees could cross the resolved-file
bound and fail committed-manifest tests (and local repo-context runs)
closed with no manifest change. Narrow that glob to source-only patterns
and extend the provider's never-descend set with Playwright artifact
directory names. Strengthen the committed-manifest tests to derive one
probe per paths pattern, pin rendered payloads and paths patterns, assert
each relatedPaths glob resolves, and gate the .gitignore un-ignore hunk on
the manifest staying un-ignored and tracked.

Co-authored-by: Qwen-Coder <[email protected]>

* test(review): pin repository context manifest policy

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-08-07 10:07:15 +00:00
cb2555c7c5 feat(desktop): package Web Shell as a release-ready desktop app (#8132)
* feat(desktop): add Web Shell Tauri proof of concept

* feat(desktop): prepare Web Shell shell for release

* fix(desktop): make release dry runs portable

* fix(desktop): harden cross-platform release smoke

* fix(desktop): stabilize Windows and Linux CI

* fix(desktop): scope bootstrap env to daemon

* fix(desktop): stabilize packaged app smoke

* fix(desktop): diagnose Linux packaged startup

* fix(desktop): address release readiness review

* fix(desktop): address follow-up review findings

* fix(desktop): address runtime review blockers

* fix(desktop): gate cookie auth acceptance behind desktop bootstrap flag

- Cookie→Bearer translation middleware now only active when desktopShellBootstrap is enabled
- Use timing-safe comparison for bootstrap token validation

* fix(desktop): replace cookie handshake with URL fragment auth

- Navigate the desktop WebView to /#token=<token>; the fragment never
  reaches the server, so drop the desktop cookie bootstrap middleware,
  its cookie->bearer translation, and the related serve tests
- Skip the deferred-runtime auth gate for pre-auth Web Shell routes
  (GET|HEAD / and /assets/*): a document navigation cannot carry an
  Authorization header, so the fast-path window used to answer the
  first desktop navigation with 401 Unauthorized until a manual reload
- Poll /health?deep=true before navigating: deep health stays 503
  (reason: bootstrap) until the runtime app that mounts the Web Shell
  is ready, so readiness can no longer race the deferred window
- Run the folder picker off the main thread and only store the runtime
  after the WebView navigation succeeds
- Enable withGlobalTauri plus a bootstrap capability so the bootstrap
  page can subscribe to desktop lifecycle events
- Update smoke-packaged to assert the fragment contract (unauthenticated
  root navigation 200, no cookies minted, API routes still 401) and
  sync the release design doc

* fix(desktop): fix Linux smoke log path, add runtime .gitkeep, correct README (#8132)

* fix(desktop): close release readiness gaps

* fix(cli): keep deferred serve auth gate closed when web shell unmounted (#8132)

* fix(desktop): address review feedback on auth gates and runtime bundle (#8132)

- Cover the method guard in isPreAuthWebShellRequest: assert unauthenticated POST to / and /assets/* is still 401 during the deferred runtime window.

- Add unit tests for is_allowed_navigation covering the unset origin, set origin, and bootstrap-after-origin cases.

- Drop DEV:'true' from the release bundle step so the esbuild metafile is no longer shipped as dead weight in the desktop runtime.

* fix(desktop): address review feedback on runtime extraction and release workflow (#8132)

- Extract .zip Node archives with unzip so Linux cross-builds for win32-x64
  no longer crash on GNU tar.
- Build the Windows signing config with ConvertTo-Json instead of backslash
  escapes, which PowerShell treats as a parse error.
- Fetch the runtime Web Shell without a bearer token so the smoke test
  exercises the pre-auth navigation path the shell relies on.
- Make GitHub release creation idempotent so a re-run after a partial publish
  uploads assets instead of failing on the existing tag.

* fix(desktop): normalize artifact filenames to prevent updater 404s (#8132)

GitHub rewrites spaces to dots when release assets are uploaded, but
the updater manifest encoded spaces as %20 via encodeURIComponent.
This caused every platform's auto-update URL to 404 on published
releases.

Replace spaces with hyphens in the Collect artifacts step for all
platforms so the local filename, the manifest URL, and the published
asset name agree by construction. Update test-release.js fixtures to
match and assert no artifact name contains a space.

* fix(desktop): address review feedback on security, lint, and code quality (#8132)

* fix(desktop): address review feedback on smoke test, error UX, and window state (#8132)

* fix(desktop): address review feedback on crate build, recovery UX, auth gate, and CI (#8132)

* fix(desktop): address review feedback on settings race, version script, and log growth (#8132)

* fix(desktop): address review feedback on retry, auth gate, and release clobber (#8132)

* fix(desktop): gate commands to bootstrap origin and show native update dialog (#8132)

* fix(desktop): use matches! instead of PartialEq on JoinError result (#8132)

* fix(desktop): wait for deferred runtime in smoke tests and sync release flags on clobber (#8132)

---------

Co-authored-by: Qwen Code Bot <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
2026-08-02 08:20:16 +00:00
Shaojin Wenandwenshao 02c79beb62 feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs (#6880)
* feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs

PRs that touch the web-shell UI now get an auto-updated comment with
light/dark screenshots of key views (transcript, slash menu, model/theme
dialogs, permission panel) and short GIF recordings of common flows,
rendered against the existing mock daemon — no real backend, no secrets.

Split into two workflows for security, since capture runs untrusted PR code:

- web-shell-visuals.yml (pull_request): checks out the PR head, builds and
  renders it with Playwright, captures PNGs + webm, converts webm->GIF with
  ffmpeg, and uploads an artifact. `contents: read` only, references no
  secrets — fork PRs run with a read-only token and no secrets.
- web-shell-visuals-publish.yml (workflow_run): downloads the artifact,
  binds it to its real PR by requiring the PR head SHA to equal the run's
  authenticated head SHA, hosts the images on a per-PR `pr-assets/*` branch
  (referenced by immutable commit SHA), and posts/updates one inline
  comment. Never checks out or runs PR code; the write token lives only here.

Capture infra is self-contained in packages/web-shell
(playwright.visuals.config.ts + client/e2e/visuals/*), reusing the mock
daemon harness. Run locally with:
`npm run test:e2e:visuals --workspace=packages/web-shell`.

* fix(web-shell): guard empty gh api response in visuals publish

Addresses review feedback on #6880: if `gh api` returns empty (network
error / rate limit), jq on empty stdin errors and `set -e` kills the
publish job. Skip gracefully instead.

* fix(web-shell): address review nits on visuals capture

- harness recordFlow: wrap video saveAs/delete in try/catch so a video
  I/O error (e.g. drive failed before navigation) can't mask the real
  driveError.
- capture workflow: drop the unused head_sha.txt artifact field; the
  publish job binds to the authenticated workflow_run.head_sha, and an
  artifact-sourced SHA would be untrusted.

* fix(web-shell): address second review round on visuals capture

- context.close() in recordFlow's finally is now best-effort (try/catch)
  so a close/crash error can't mask the real driveError.
- add a flows spec that asserts a throwing drive propagates its own error.
- trigger the capture workflow on playwright.visuals.config.ts changes too.

* fix(web-shell): address third review round on visuals capture

- harness: log (don't silently swallow) a video save/null when drive
  succeeded; keep masking-suppression only when driveError is set.
- publish: HTML-escape interpolated values in the comment builder (defense
  in depth, independent of the upstream filename sanitization); fix the
  stale 'single pr-assets branch' comment and key concurrency on source
  repo+branch so different PRs (incl. same-named fork branches) parallelize.
- capture: bump checkout to v6.0.3 (repo standard); surface ffmpeg's stderr
  on GIF-conversion failure instead of discarding it.

* fix(web-shell): harden visuals publish/capture (review round 4)

Publish (privileged workflow_run):
- CRITICAL: capture basename before `tr` so its trailing newline isn't
  turned into `_` (which broke the .png/.gif filter -> empty preview).
- dedup only against the bot's OWN comment (author + marker), not any
  marker-bearing comment a participant can post.
- bound the pr-assets branch: force-push a single orphan snapshot per run
  (previous snapshot GC'd) instead of appending unbounded untrusted content;
  this also removes the rebase/retry path.
- cap EXAMINED candidates (not just accepted) before validation; tighten
  per-file (3MiB) and accepted-image (14) caps.
- re-validate PR open + head-SHA immediately before the comment write
  (TOCTOU); retry the comment listing and abort rather than POST a duplicate
  when listing fails.
- esc() the runUrl for consistency with the self-defending HTML.

Capture (pull_request):
- upload raw recordings as a SEPARATE artifact the publisher never downloads,
  so an untrusted multi-GB video can't exhaust the privileged job.
- also trigger on packages/webui/src and packages/sdk-typescript/src (the
  visuals dev server aliases them).
- create screenshots/gifs dirs before the metadata counts (defensive).

Harness recordFlow:
- track drive failure with an explicit boolean (handles `throw undefined`);
  discard the recording on failure so a failed flow leaves no bogus webm.

* refactor(web-shell): extract + unit-test the visuals publish staging/comment

Addresses the review's testability gap (the class of bug that let the
filename sanitizer break the whole preview slip through green CI). The image
validation (magic bytes, filename sanitization, examined/accepted/size caps)
and the comment builder (light/dark pairing, flow labels, HTML escaping) move
from inline workflow bash/node into .github/scripts/web-shell-visuals-publish
.mjs, covered by web-shell-visuals-publish.test.mjs (run in ci.yml's
node --test line). The publish workflow sparse-checks-out and calls the
script instead. Behaviour is unchanged; it just gained a test surface.

* fix(web-shell): retry the visuals asset force-push; drop stale comment

Round-4 switched hosting to a force-push but left a comment referencing a
'push-retry loop' that no longer existed, and the force-push was a single
call that set -e would abort on a transient failure. Add a bounded retry and
correct the comment.

* fix(web-shell): harden visuals publish/capture (review round 6)

Script (unit-tested):
- flow labels: own-property lookup so `toString.gif`/`constructor.gif` can't
  leak Object.prototype members into the comment.
- per-kind image caps (screenshots vs gifs) so a large screenshot set can't
  silently starve the flow GIFs from the preview.
- tests for both, plus the per-kind cap.

Publish:
- bind the artifact PR number to the run's authenticated head repo+branch
  (not just head SHA), rejecting a sibling PR that shares the same commit.
- re-validate before the force-push and again right before the comment write
  (close the download/stage/lookup TOCTOU windows).

Capture:
- bound artifact contents before upload (drop oversized / excess files) so an
  untrusted spec can't bloat the published or video artifact.
- trigger on the capture workflow file itself.

- new close-trigger cleanup workflow deletes a PR's asset branch on close, so
  pr-assets/* refs don't accumulate without bound.
- single-source the capture viewport (constants.ts) shared by config + harness.
- model-switch flow asserts the daemon model request actually fired.

* fix(web-shell): stricter visuals error handling (review round 7)

Harness recordFlow:
- when the drive SUCCEEDS, a failed context.close() or video.saveAs() (or a
  missing recording) now FAILS the flow instead of a swallowed console.warn —
  a silent pass with no .webm makes the downstream GIF step fail confusingly.
  A drive FAILURE still discards the partial video and rethrows the original
  error (unchanged).

Publish:
- validate_pr distinguishes a transient API failure (empty after retries ->
  exit 1, re-triggerable) from a genuine invalid state (closed / head mismatch
  -> skip), via a `gate` wrapper used at all three checkpoints.
- add a 2s backoff between comment-listing retries (matching the push retry).

---------

Co-authored-by: wenshao <[email protected]>
2026-07-15 06:48:52 +00:00
顾盼andShaojin Wen 24b2442875 feat(cli): add /learn command for user-initiated skill creation (#6440)
* feat(cli): add /learn command for user-initiated skill creation

Add a /learn slash command that lets users explicitly point the agent at
knowledge sources (local dirs, URLs, conversation history, or freetext)
to create reusable skills.

Implementation uses submit_prompt (same pattern as /remember) to let the
main model gather knowledge and write a SKILL.md in the normal turn.
Skills are saved under .qwen/skills/learned-skill-<name>/ with
source: learned frontmatter for edit isolation from auto-skills and
user-authored skills.

Also includes a forked-agent path (runLearnSkillByAgent) with scoped
permissions for future ACP/auto-learn scenarios, plus 29 unit tests.

* fix(i18n): add zh/zh-TW translations for /learn command description

CI strict-parity check requires all built-in command descriptions to
have translations in zh-CN and zh-TW locales.

* fix(i18n): add en.js baseline key for /learn command description

The check-i18n script requires all translated keys to also exist in the
English baseline locale file.

* refactor(core): remove unused forked-agent infrastructure, add collision guard

Address review feedback on PR #6440:

- Remove ~300 lines of unused forked-agent code (runLearnSkillByAgent,
  createLearnSkillScopedAgentConfig, scoped permissions, system prompt,
  buildLearnTaskPrompt) — no callers in production code.
- Make buildLearnSkillPrompt async and add listExistingSkillDirNames
  enumeration to prevent skill name collisions.
- Revert buildAgentHistory export (no longer needed).
- Trim test suite from 29 to 10 tests (dead code tests removed).

* feat(config): default auto-skill to disabled

With /learn available for explicit skill creation, auto-skill (background
skill extraction after 20 tool calls) is no longer needed as the default
experience. Users who want automatic extraction can re-enable it via
memory.enableAutoSkill in settings.

* feat(config): align auto-skill default to disabled across all layers

The prior commit only changed the core Config constructor default.
CLI config builder, ACP agent defaults, and settings schema still
defaulted to true, making the core change unreachable for CLI users.

Align all layers:
- cli/config.ts: ?? true → ?? false
- acpAgent.ts: enableAutoSkill default true → false
- settingsSchema.ts: default true → false
- read-file.ts: resolve stale merge conflict markers

* chore: regenerate settings.schema.json for auto-skill default change

* test: update tests for auto-skill default change, fix stale conflict resolution

- config.test.ts / acpAgent.test.ts: update assertions for the
  enableAutoSkill default flip (true -> false) from the prior commit.
- read-file.ts: an earlier rebase conflict resolution incorrectly kept
  os.tmpdir() as an allowed read root — it was removed on main. Restore
  the main version so read-file.test.ts's "ask for OS temp paths" case
  passes again, and drop the now-unused os import.

* fix(core): restore dynamic ignore-file message, wrap /learn input as opaque data

- read-file.ts: an earlier rebase conflict resolution incorrectly hardcoded
  the ignore-file error message to '.qwenignore', losing the dynamic
  getQwenIgnoreFileDisplayForPath() call that reports the actual file
  (.agentignore, .cursorignore, etc.) that blocked the read. This was the
  real cause of the read-file.test.ts CI failures — not a pre-existing bug
  as previously assumed.
- learn-skill-agent.ts: wrap the /learn command's raw user input in
  <user_data> tags with an explicit "don't follow instructions" preamble,
  matching the existing <user-content> pattern in remember.ts. Since
  buildLearnSkillPrompt feeds submit_prompt (full tool access), unwrapped
  content fetched from a URL could otherwise redirect tool calls.

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-07-15 05:30:38 +00:00
5c82857fea Add harness infrastructure for web-shell package (#6517)
* test(web-shell): add browser and lint harness

* test(web-shell): harden browser smoke harness

* fix(web-shell): guard mock daemon model state

* test(web-shell): remove unused scenario harness

* fix(web-shell): remove stale lint disables

* test(web-shell): make matchMedia stub writable

* fix(web-shell): exclude tests from package typecheck

* test(web-shell): tighten mock daemon route contract

* Update packages/web-shell/client/e2e/utils/mockDaemon.ts

Co-authored-by: qwen-code-ci-bot <[email protected]>

* test(web-shell): clear stale SSE connections

* ci(web-shell): gate smoke on full CI profile

---------

Co-authored-by: ermin.zem <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: 易良 <[email protected]>
2026-07-09 08:11:58 +00:00
MikeWang0316twandClaude Opus 4.8 1b58ede8e7 fix(cli): smoother live streaming preview — drop "generating more" cue, hold back partial table rows (#6340)
* fix(cli): drop redundant "generating more" cue from the live preview

In non-VP mode the live markdown preview is clipped to a rendered-height
budget so the frame never overflows the viewport and triggers ink's
scroll-to-top full redraw. It used to append a "... generating more ..."
cue (and code/math/mermaid blocks appended their own) to signal that the
clipped tail was still coming.

Since #6170 landed the incremental scrollback commit, that tail is
streamed into <Static> in real time — clipped content is "still
streaming" and reappears within a commit cycle, not "delayed output".
The cue is therefore redundant noise that flickers in step with the
commit cycle, so remove all four occurrences (outer preview clip, code
block, mermaid block, math block).

The row each cue used to occupy is reclaimed for content, so the total
rendered height is unchanged: the code/math/mermaid RESERVED_LINES drop
by one and the outer slice trigger switches from the (now-inlined)
`clipped` flag to `keptLines < allLines.length`. The TableRenderer
"… more rows streaming …" clamp is intentionally kept — an in-progress
oversized table is not yet in scrollback, so that cue still carries
information.

Also gitignore the nested `.qwen/computer-use/` marker so the
auto-generated artifact stops showing up as untracked.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(cli): hold back the unterminated table row while streaming

While a markdown table streams, the frontier line is often a half-typed
row like `| a | b` with no closing `|` yet. Because TABLE_ROW_RE requires
both a leading and trailing pipe, that partial line does not match, so the
parser closed the table and rendered the partial as a plain text line
below it — then, once the closing `|` arrived, flipped it into the table.
This per-token flip changed the frame height and re-ran column autosizing
on every keystroke, jittering the live table.

Hold the partial row back instead: when pending, if the final line is an
unterminated table row and at least one complete row already exists, skip
it so `inTable` stays set and the end-of-content handler keeps rendering
the accumulated rows as a live table. The row appears the moment it
terminates. The `tableRows.length > 0` guard keeps the header + separator
from blanking out while the very first row is still being typed.

Note: this smooths the table content itself; it does not change the
streaming repaint frequency, so the fixed bottom controls still repaint on
each tick (that is the domain of the flicker-reduction work, e.g. #5396).

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* docs(cli): fix two stale "generating more" cue references in comments

Review follow-up: two comments still referenced the removed outer cue.

- TABLE_PENDING_RESERVED_ROWS: reword "marginY 2 + the outer cue" to
  "marginY 2 + one row of wrapped-cell safety headroom". The reserve stays
  at 3 on purpose — tables under-estimate their rendered height the most
  (wrapped cells), so they keep one more backstop row than the other
  blocks; lowering it would shrink that safety margin.
- pending-rendered-height PendingSliceResult.keptLines JSDoc: drop the
  "plus a 'more' cue" phrasing — the caller now renders nothing rather
  than an oversized row.

Comment-only; no behaviour change. 155 tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(cli): hold back the partial first table row too; de-dup reserve constant

Review follow-up on the streaming table hold-back.

- The `tableRows.length > 0` guard skipped the hold-back for the FIRST
  data row: a partial first row fell through to the table-closing branch,
  which also requires a row, so the header + separator were dropped and
  the partial rendered as a stray text line — the same per-token flip the
  change is meant to remove, just for the first row. Relax the guard to
  `tableHeaders.length > 0` so an unterminated first row/separator is held
  back too; the table is simply not drawn until its first row terminates,
  then pops in complete and grows one row at a time. Comment corrected to
  describe the actual behaviour.
- Add a test for that edge case (partial first row held back, table
  appears once the row terminates).
- De-duplicate the magic `3`: the slice-side `tableClampRows` estimate now
  references `TABLE_PENDING_RESERVED_ROWS` (moved to the top-of-file
  constants) instead of a literal, so the estimate and RenderTable's
  render-side `maxHeight` cap can never diverge.

157 tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 <[email protected]>
2026-07-05 15:56:48 +00:00
PilgrimStackandatlarix-agent ea0749ead8 feat(web-shell): add daemon UI support for vision model selection (#6209)
* feat(web-shell): add daemon UI support for vision model selection

Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)

* feat(web-shell): add daemon UI support for vision model selection

Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)

* Updated .gitignore

Co-authored-by: atlarix-agent <[email protected]>

* adding atlarix to gitignore

* chore: remove .atlarix/.atlarixignore from tracking

* chore: revert unrelated .gitignore, package-lock.json, and NOTICES.txt changes

These were local environment artifacts accidentally staged alongside the
/model --vision feature. Reverting to keep the PR focused.

- Restore .gitignore to base state
- Restore package-lock.json (fsevents peer flag, test-utils entry)
- Restore NOTICES.txt (hasown version)

* Updated .gitignore

Co-authored-by: atlarix-agent <[email protected]>

* chore: add .atlarix/ to .gitignore

Prevents Atlarix workspace files from being tracked in the repo.

* Updated .gitignore

Co-authored-by: atlarix-agent <[email protected]>

---------

Co-authored-by: Qwen3.6 Plus agent <[email protected]>
2026-07-03 04:31:11 +00:00
ca61d7827e feat(channels): add identity and task lifecycle metadata (#6105)
* chore: ignore local worktrees

* docs(channels): design identity and task lifecycle p0

* docs(channels): plan identity and task lifecycle p0

* feat(channels): add identity and task lifecycle metadata

* fix(channels): suppress cancelled tool call lifecycle

* fix(channels): cover loop lifecycle metadata

* fix(channels): harden lifecycle event edges

* fix(channels): finalize cancelled lifecycle before cleanup

* fix(channels): address lifecycle review suggestions

* fix(channels): close lifecycle cancellation races

* fix(channels): separate pending cancel state

* fix(channels): order clear cancellation lifecycle

* fix(channels): suppress loop chunks during pending cancel

* test(channels): use active session in cancel regression

* fix(channels): sanitize lifecycle tool fields

* fix(channels): route shared tool call lifecycle

* fix(channels): preserve pending cancel intent

* fix(channels): preserve responses after failed cancel

* fix(channels): tighten lifecycle cancellation reasons

* fix(channels): close lifecycle cancel races and validate identity config

Address the outstanding review findings on #6105:

- carry a typed reason on ChannelLoopSkippedError and report disabled
  loops as 'dropped' instead of 'timeout'
- treat a turn as committed once delivery starts: /cancel re-checks
  deliveryStarted after the cancel RPC settles, and neither prompt path
  lets a late-settling cancel rewrite a delivered turn into cancelled
  (or follow a /clear cancellation with completed)
- tag failed lifecycle events with phase: agent vs delivery
- validate identity/memoryScope shape at config parse time instead of
  throwing an opaque TypeError on the first prompt of every session
- guard onPromptStart hooks, pass job.id to loop onPromptStart/End,
  append the boundary block after operator instructions, gate
  /who + /status identity lines on configured identity/memoryScope,
  cache the boundary prompt, and route error logs through
  lifecycleError()

Co-authored-by: Qwen-Coder <[email protected]>

* fix(channels): keep failed terminal event when cancel settles post-delivery

Self-review follow-up: once delivery started, the catch paths no longer
reconcile a pending cancel — a late-resolving cancel RPC used to flip
cancelled=true there, suppressing the failed emit while the /cancel
handler (seeing deliveryStarted) also declined to emit, leaving a
started task with no terminal lifecycle event at all.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(channels): hold streamed chunks while a cancel is pending

Chunks arriving while a /cancel RPC is in flight were pushed straight
into the BlockStreamer, which can send a block on a size/paragraph
threshold before the cancel resolves — leaking output a successful
cancel can never recall. Hold the pending-window chunks instead: replay
them (block streaming + text_chunk transcript) when the cancel fails,
discard them when it succeeds. onResponseChunk stays live through the
window so adapter-accumulated display state has no permanent hole on a
failed cancel; adapters gate visible updates on their own stop flags.

Also stop passing the loop job id to onPromptStart/onPromptEnd (and the
/clear eviction path): the hook contract is inbound platform message
ids, and adapters act on them — cards and reactions keyed to a fake id.
Lifecycle events still carry job.id for correlation.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(channels): defer adapter chunks while cancel is pending

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-07-02 09:04:07 +00:00
qqqysandqwen-code-dev-bot 6a5ad453c8 [codex] Add explicit channel memory for messaging channels (#6051)
* chore: ignore local worktrees

* feat(core): add channel memory store

* fix(core): preserve dots in channel memory paths

* fix(core): guard dot channel memory paths

* feat(channels): add channel memory commands

* fix(channels): defer channel memory session invalidation

* feat(channels): inject channel memory into sessions

* fix(channels): serialize channel memory context injection

* fix(channels): harden channel memory context races

* fix(channels): let queued turn retry memory context

* feat(cli): wire channel memory into channel startup

* docs(channels): document channel memory

* fix(channels): harden channel memory handling

* fix(channels): gate channel memory injection

* fix(channels): serialize channel memory clears

* fix(channels): tolerate channel memory read failures

* fix(channels): harden channel memory review gaps

* fix(channels): harden channel memory review gaps

* fix(channels): protect shared channel memory

* test(channels): allow group memory view test

* fix(channels): block group memory writes

* fix(channels): include memory in loop prompts

* fix(channels): preserve session context order after merge

* fix(channels): retry loop memory context after read failure

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-07-01 09:59:48 +00:00
7b9e31885b feat(web-shell): add mobile sidebar drawer with session list (#6003)
* feat(web-shell): add mobile sidebar drawer with session list

Replace the display:none behavior at viewport <=760px with an overlay
drawer pattern. A hamburger menu button appears on mobile, tapping it
slides the existing WebShellSidebar in as a fixed overlay with a
semi-transparent backdrop. Selecting or creating a session auto-closes
the drawer. Desktop layout (>=761px) is unaffected.

Closes #6000

* fix(web-shell): address review feedback for mobile sidebar drawer

- Use display:contents for desktop wrapper transparency (Critical: sidebar was hidden)
- Fix z-index stacking so sidebar renders above backdrop in drawer
- Force sidebar expand when mobile drawer is open (collapsed state)
- Hide resizeHandle on mobile to prevent touch scroll conflicts
- Reset drawer state on viewport resize via matchMedia listener
- Add role=dialog, aria-modal, Escape key dismissal, body scroll lock
- Add aria-expanded to hamburger button
- Close drawer when opening Settings or resuming sessions

* fix(web-shell): address second round of review feedback

- Remove dead :global(.sidebar) selector (CSS Modules hash class names)
- Fix Escape key capture-phase handler to not intercept sidebar inputs
- Conditionally apply role=dialog/aria-modal only when drawer is open
- Stop toggling collapsed prop on drawer open/close to preserve sidebar state
- Add closeMobileDrawer() for bare /resume command path
- Fix hamburger button vertical centering in empty chat state on mobile

* fix(web-shell): fix stacking context and escape handler in mobile drawer

* fix(web-shell): prevent iOS Safari background scroll when drawer is open

* chore: remove accidentally committed .qwen-session and gitignore it

The .qwen-session file is a developer-local session UUID generated by
qwen serve. It was accidentally committed to the repo and should never
be tracked.

* fix(web-shell): address review feedback for mobile drawer

- Don't preventDefault touchmove inside the drawer so the session list
  can scroll natively; only block scrolling on the page behind it.
- Defer Escape to a pending tool/permission approval (reject) instead of
  closing the drawer when a prompt is visible.
- Reuse isEditableTarget from utils/dom and only bail out for editable
  targets outside the drawer, so the drawer search input still closes on
  the first Escape.
- Close the drawer before awaiting loadSession so it doesn't linger over
  the old transcript, matching the other session-switch paths.
- Keep the drawer panel visible until the backdrop finishes fading out to
  avoid a one-frame flicker on close.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(web-shell): mobile drawer ignores collapsed rail + block backdrop scroll

- collapsed: a user who collapsed the desktop sidebar got a mobile drawer that
  still rendered as the icon rail (no session list — the whole point of the
  drawer). Force the expanded layout while the drawer is open.
- touchmove: the allowlist matched the outer [data-mobile-drawer] wrapper, which
  also contains the full-screen backdrop, so a touchmove starting on the dim
  backdrop skipped preventDefault and let iOS Safari scroll the page behind.
  Exclude the backdrop so only the panel keeps native scroll.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(web-shell): harden mobile drawer collapse, error path, and width cap

- Hide the sidebar collapse button while the mobile drawer is open so its
  no-op toggle can no longer silently persist desktop collapsed state.
- Close the drawer before awaiting createSession() so a failed create no
  longer leaves the drawer stuck open with page scroll locked.
- Drop redundant width/min-width/position from .sidebar.mobileOpen and cap
  it with max-width:100vw so a wide persisted width can't overflow phones.

Co-Authored-By: Qwen-Coder <[email protected]>

---------

Co-authored-by: pomelo-nwu <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-06-30 15:34:10 +00:00
5581424b6b feat(browser-ext): revive Chrome extension via daemon-direct architecture (#5777)
* feat(chrome-qwen-bridge): 🔥 init chrome qwen code bridge

* chore(chrome-qwen-bridge): connect

* chore(chrome-qwen-bridge): connect & them

* chore(chrome-qwen-bridge): connect & them

* chore(chrome-qwen-bridge): wip  use chat ui

* chore(chrome-qwen-bridge): wip  use chat ui

* wip

* refactor(chrome-extension): rename chrome-qwen-bridge package to chrome-extension

* feat(chrome-extension): enhance network monitoring with webRequest API

Replace the existing network monitoring implementation with a more comprehensive solution that combines both webRequest and debugger APIs for broader coverage. The new implementation:

- Uses chrome.webRequest.onBeforeRequest to capture all outgoing requests
- Uses chrome.webRequest.onCompleted to capture completed responses
- Uses chrome.webRequest.onErrorOccurred to capture failed requests
- Retains debugger API integration for detailed network information
- Implements memory management with maximum 1000 logs per tab
- Adds proper initialization and cleanup for each tab
- Ensures graceful handling of debugger attachment failures
- Provides more reliable network activity capture across all tabs

This enhancement significantly improves the reliability and coverage of network monitoring functionality in the Chrome extension.

* refactor(chrome-extension): reorganize directory structure for better maintainability

This commit reorganizes the entire Chrome extension package structure for improved maintainability and clarity:

- Move all source files to `src/` directory (background, content, sidepanel)
- Move build configurations to `config/` directory
- Move documentation to `docs/` directory with proper categorization
- Move all script files to `scripts/` directory
- Move native-host specific files to appropriate subdirectories (`src`, `scripts`, `config`)
- Update package.json scripts to reflect new file locations
- Add comprehensive documentation files (debugging, development, architecture, API reference)
- Maintain all functionality while improving project organization

The reorganization separates source code from build output, centralizes documentation, and creates a clear separation of concerns making the project more maintainable and easier for developers to navigate.

* feat(chrome-extension): enhance native host communication and network logging

- Add troubleshooting documentation for native host setup issues
- Improve native host logging to home directory with fallback to tmp
- Enhance network logging in service worker with response body capture
- Update scripts to properly reference host.js from correct path
- Increase timeout for MCP session creation and long prompts from 3 to 5 minutes
- Add getConsoleLogs functionality to sidepanel for content script capture
- Improve browser-mcp-server network logs aggregation by request ID
- Update icon assets and improve manifest configuration

refactor(chrome-extension): consolidate host.js entry point and improve path resolution

- Create unified host.js entry point that delegates to src/host.js
- Improve path resolution for host scripts in installer and runner scripts
- Add proper path existence checks for browser-mcp-server.js
- Support running from different directory structures

style(chrome-extension): improve TypeScript type safety and error handling

- Add proper type definitions for message handling in side panel
- Add null checks and error handling for message parsing
- Improve React component callback implementations

* refactor(chrome-extension): redesign build workflow

* fix(chrome-extension): resolve ESLint errors in native host, service worker, and content script

- Fix 'Unexpected lexical declaration in case block' by wrapping switch cases in blocks
- Fix 'Unexpected constant truthiness on the left-hand side of a || expression' by using conditional patterns
- Fix unused variable errors by properly using catch error parameters or adding logging
- Fix 'document' and 'window' not defined errors in service worker with proper global declarations
- Add eslint-disable comments where appropriate for globals used in specific contexts

* feat(chrome-extension): enhance native host with browser MCP tools and event streaming

- Add new browser MCP tools: browser_click, browser_click_text, browser_run_js, browser_fill_form_auto
- Implement SSE (Server-Sent Events) for improved event streaming instead of long-polling
- Add daemon script for running the bridge host in background
- Enhance documentation with MCP notes and updated README
- Add multiple executable binaries to package.json: chrome-browser-mcp, qwen-bridge-host
- Improve error handling and event processing in the native host
- Add debouncing mechanism for stream end events in service worker
- Update timeout for MCP discovery to accommodate slower startup

Co-authored-by: Claude Opus 4.5 <[email protected]>

* fix(chrome-extension): use trusted cwd for MCP tool discovery

The root cause of MCP tools not being recognized by Qwen CLI was that
the cwd (current working directory) was defaulting to '/' (root directory).

In Qwen CLI's MCP discovery logic, there's a security check:
  if (!cliConfig.isTrustedFolder()) {
    return;  // Skip MCP tool discovery
  }

The root directory '/' is not a trusted folder, so MCP tools were
silently not being discovered at all.

Changes:
- host.js: Default to $HOME instead of process.cwd() for start_qwen
- service-worker.js: Remove '/' fallback, let host.js handle default

This ensures browser MCP tools (browser_read_page, browser_click, etc.)
are properly discovered and available to the model.

* fix(core): validate MCP entry script existence before connection

Add pre-flight check to verify that stdio-based MCP server entry scripts
exist on disk before attempting connection. This prevents silent failures
and provides clear error messages for misconfigured MCP servers.

* fix(chrome-extension): enhance native host path resolution and port config

- Add QWEN_BROWSER_MCP_SERVER_PATH env override for custom installations
- Expand candidate search paths for browser-mcp-server.js discovery
- Add detailed logging when MCP server script is not found
- Support BRIDGE_PORT env variable for HTTP API server configuration

* fix(chrome-extension): improve browser MCP server reliability and debugging

- Add comprehensive debug logging for bridge health checks and host spawn
- Support BROWSER_MCP_NO_SPAWN env to disable automatic host.js spawning
- Handle bridge unavailability gracefully with clear error messages
- Add raw JSON mode fallback for clients without Content-Length framing
- Capture and log host.js stdout/stderr instead of inheriting stdio
- Add pre-flight bridge check at startup for better diagnostics
- Handle each bridge call failure with proper error responses

* chore(chrome-extension): add debug wrapper script for MCP server

Add cbmcp-wrapper.sh to help diagnose MCP server invocation issues.
The wrapper logs invocation details and stderr to /tmp/cbmcp.log,
making it easier to debug when Qwen CLI spawns the MCP server.

* docs(chrome-extension): add MCP/Bridge troubleshooting guide

Document common failure scenarios when MCP bridge shows as Disconnected:
- EPERM errors when spawning host.js cannot bind to port
- Content-Length framing issues during MCP handshake
- Step-by-step troubleshooting with flow diagrams
- Manual bridge setup with BROWSER_MCP_NO_SPAWN workaround

* build(chrome-extension): 浏览器插件 mcp 构建优化

* docs(chrome-extension): update docs

* fix(chrome-extension): 解决CDP响应体获取和权限请求处理问题

* feat(mcp-chrome-integration): add MCP Chrome browser extension integration

Add complete Chrome extension with native messaging host for MCP integration:
- Chrome extension with sidepanel UI, service worker, and content script
- Native server with agent engines (Claude/Codex), session management, and tool bridge
- Shared packages for types, tools, and node specifications
- Documentation and build scripts

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* chore(mcp-chrome-integration): refactor

* feat(chrome-extension): 切换到HTTP后端代理并更新构建配置

* refactor(mcp-chrome-integration): build

* wip

* wip chrome extension

* chore: ignore .worktrees

* docs: plan sidepanel component removal

* refactor(sidepanel): remove local components

* feat(chrome-extension): add native messaging ACP client and protocol support

- Add ACP client for native messaging communication
- Add file handler for local file access via native host
- Add protocol definitions for ACP communication
- Archive old documentation files
- Update integration status and protocol documentation

* fix(chrome-extension): use GenericToolCall from @qwen-code/webui

- Replace non-existent local ToolCallCard import with GenericToolCall
- Import from @qwen-code/webui package instead of local path
- Fix component props to match GenericToolCall interface
- Remove unused ToolCallData import

* refactor(mcp-chrome-integration): split large files and fix ESLint errors

- Split tools.ts (1554 lines) into 8 schema files by functionality
- Split native-messaging.ts (1533 lines) into 6 modules
- Split doctor.ts (1099 lines) into 8 modules
- Split content-script.ts (1055 lines) into 5 modules
- Add Qwen Team license headers to all new files
- Remove unused tool names (SEARCH_TABS_CONTENT, SEND_COMMAND_TO_INJECT_SCRIPT, USERSCRIPT, RECORD_REPLAY)
- Fix ESLint errors: no-require-imports, no-explicit-any, no-unused-vars, prefer-const
- Add archive/ directory to eslint ignore patterns

* 调试 MCP 工具成功

* refactor: revert unnecessary formatting changes and clean up MCP chrome-integration

- Revert Node version requirement from >=22 to >=20
- Revert code formatting changes (import statements and indentation)
- Archive obsolete chrome-extension implementation
- Clean up outdated documentation and scripts
- Reorganize MCP chrome-integration docs

* docs(mcp-chrome): 新增核心文档和更新 README

- 新增 02-features-and-architecture.md (27个工具完整参考)
- 新增 03-design-and-implementation.md (与 hangwin/mcp-chrome 对比)
- 新增 04-test-cases.md (35个测试用例)
- 更新 01-installation-guide.md (Extension ID 固定方案)
- 重写 README.md (对齐新文档结构)

* refactor(chrome-extension): 简化 sidepanel 并移除未使用代码

- 删除未使用的 Onboarding 组件
- 删除冗余样式文件 (App.css, timeline.css)
- 删除未使用的工具函数 (diffStats, diffUtils, sessionGrouping, tempFileManager, webviewUtils)
- 新增 MCP 工具状态横幅显示
- 隐藏不需要的 UI 按钮 (slash command, attach, edit mode)
- 移除未使用的变量 (clearToolCalls)
- 更新 manifest.json 和 native-messaging-host

* chore: regenerate lockfile for mcp-chrome-integration workspace

npm install reconciles the lock with the merged package.json: adds the
new packages/mcp-chrome-integration/app/* workspace subtree (371 deps).
No existing main dependencies were removed.

* chore(chrome-integration): drop dead logger + unused pino deps

native-server/src/util/logger.ts was 100% commented-out dead code (even
a hardcoded /Users/hang/... path) with zero importers; pino/pino-pretty
were declared but never imported (logging is console.error to stderr,
correct for a native-messaging host). Remove the file and both deps.

* feat(chrome-integration): WIP serve-backed agent client (Phase 1 backbone)

Replace the hand-rolled ACP client by driving `qwen serve` (main's
maintained HTTP daemon) through the SDK's DaemonClient. New
serve-agent/client.ts spawns `qwen serve --no-web` where the host used to
spawn `qwen --acp`, then uses DaemonClient (REST + SSE) for session
create / prompt / cancel / permission / streaming. Same public surface as
AcpClient so the native host can swap in place.

Compiles + typechecks against the real SDK API. NOT yet wired into
native-messaging-host.ts — wiring is gated on two items that need runtime
validation in a real Chrome + qwen env:
  - OAuth: host's onAuthenticateUpdate (in-extension authUri) maps to
    serve's server-side device-flow events — needs design.
  - permission requestId becomes string (was number) — host's
    permissionRequests map + handler need to follow.

acp/ kept in place until the serve path is validated. Packaging follow-up:
@qwen-code/sdk (→ core) as a dep of the standalone host is fine in the
monorepo but needs a publish-time story (bundle or resolve from the
co-installed qwen).

* feat(browser-ext): daemon-direct connection foundation (Phase 1, #5626)

First brick of the daemon-direct architecture from #5626: the extension
talks straight to a local `qwen serve` HTTP daemon instead of a native
messaging host.

- daemon/config.ts: resolve { baseUrl, token } — default loopback
  http://127.0.0.1:4170 (auth-free), overridable via chrome.storage.local.
- daemon/discovery.ts: GET /health probe so the side panel can show a
  "start qwen serve" hint instead of a broken chat when no daemon is up.

Both typecheck clean. (Pre-existing tsc errors live only in the orphaned
legacy sidepanel hooks — useWebViewMessages/useToolCalls etc. — which the
DaemonSessionProvider migration removes next.)

* docs(browser-ext): daemon-direct architecture spec (Phase 1+2, #5626)

Concrete implementation spec: Phase 1 (side panel as daemon client, no
daemon changes) and Phase 2 (browser tools as a client-hosted MCP server
over the daemon WS). Phase 2 reuses the existing SdkControlClientTransport /
SdkControlServerTransport pattern but moves the wire from the SDK subprocess
control plane onto qwen serve's WebSocket — a new public daemon-contract
surface, gated behind a capability flag (the open question in #5626).
Includes the daemon-lifecycle options for #5626 Q3.

* feat(browser-ext): Phase 1 — side panel as a daemon-direct client (#5626)

Side panel chat now talks straight to a local `qwen serve` HTTP daemon via
@qwen-code/webui's DaemonSessionProvider, replacing the native-messaging
relay (background/ + content/ untouched; nativeMessaging perm kept for now).

- SidePanelRoot.tsx: health gate — checkDaemonHealth(getDaemonConfig());
  shows a "run qwen serve" hint + Retry when unreachable, else mounts
  DaemonSessionProvider around App.
- App.tsx: rewritten daemon-driven — transcript/streaming/permissions/
  lifecycle from the webui daemon hooks (useTranscriptBlocks, useStreamingState,
  usePromptStatus, usePendingPermissions, useConnection, useActions); reuses
  the existing webui presentational components + ChromePlatformProvider.
- sidepanel/daemon/{transcriptItems,permission}.ts: adapters from daemon
  transcript/permission shapes to the existing UI components.
- Deleted the dead legacy native-messaging chat hooks/types.

Verified: sidepanel/daemon tsc clean; `npm run build` (esbuild) passes,
emits the side-panel bundle with the daemon wiring. (Pre-existing tsc errors
remain only in background/ + content/, out of scope until Phase 2.)

Daemon contract accepted live against the worktree `qwen serve`: /health,
/capabilities (advertises session_create/prompt/events/workspace_mcp),
POST /session runs end-to-end to the model-auth gate.

* feat(browser-ext): Phase 2 — browser tools over the daemon WS (#5626)

Reverse tool channel: a WS client (the extension) hosts an MCP server (its
browser tools) that the daemon's agent can call, carrying mcp_message
JSON-RPC frames over the daemon WS — reusing the SdkControlClientTransport /
SDK-MCP-server control-plane pattern. Gated behind capability flag
`client_mcp_over_ws` (opt-in; the public-contract piece flagged in #5626).

Daemon (core + cli/serve):
- core/tools/client-mcp-registrar.ts: ClientMcpRegistrar — id-correlation,
  pending/timeout, notifications fire-and-forget, exposes the
  sendSdkMcpMessage(server,msg) callback McpClientManager consumes.
- cli/serve/acp-http/client-mcp-ws.ts: ClientMcpWsConnection — handles
  mcp_register/mcp_message/mcp_unregister frames; pushes mcp_message down the
  WS; disposes on close. Hookup to the live agent McpClientManager is a
  ClientMcpServerProvider injection point (returns structured `not_wired`
  until the child↔parent reverse-IPC lands — see below).
- capability `client_mcp_over_ws` threaded through serve options/capabilities.

Extension (chrome-extension/background):
- browser-tools-server.ts: minimal hand-rolled MCP JSON-RPC
  (initialize/tools/list/tools/call) reusing the existing tool-catalog +
  router + executors (MVP 6 read-first tools); WS client to the daemon /acp
  with mcp_register + reconnect. Wired into the service worker behind a
  health probe; native messaging left intact.

Self-accepted (no LLM needed): 10/10 tests pass — a headless ws client
registers + answers the MCP handshake over mcp_message and the daemon
lists+CALLS the client-hosted chrome_read_page tool end-to-end over a real
socket. Builds: core + cli + extension esbuild all green; tsc clean.

NOT yet wired (out of scope here; needs acp-bridge + acpAgent reverse IPC):
the parent-process WS ↔ ACP-child McpClientManager hookup — the daemon's WS
lives in the parent serve process but sendSdkMcpMessage binds in the ACP
child. Single injection at the mountAcpHttp call site once that IPC exists.

* feat(serve): wire client-MCP-over-WS to the ACP child agent (#5626)

Closes the Phase 2 gap: a client-hosted (extension) MCP server's tool calls
now reach the agent's McpClientManager in the ACP CHILD, routed back to the
parent's ClientMcpRegistrar and out over the daemon WS. Opt-in via
QWEN_SERVE_CLIENT_MCP_OVER_WS=1 (the contract is still settling — dormant by
default; this is the public-daemon-contract piece flagged in #5626).

Contract additions:
- ACP ext-method `qwen/control/client_mcp/message` (child→parent, called UP):
  {server,payload} → {payload}; notifications resolve with a synthetic ack.
- runtime-MCP config flag `__clientMcpOverWs`: parent stamps it on the SDK-type
  add config; child KEEPS type:'sdk' (instead of stripping) so it binds an
  SdkControlClientTransport instead of the SDK subprocess control plane.
- BridgeOptions.clientMcpSender seam + ServeAppDeps.clientMcpSenderRegistry.

Round-trip: mcp_register(WS) → serve registers the connection's
ClientMcpRegistrar.sendSdkMcpMessage in a process ClientMcpSenderRegistry +
bridge.addRuntimeMcpServer(type:'sdk',__clientMcpOverWs) → child adds the
SDK server + runs initialize/tools/list, each frame child→parent via
client_mcp/message → BridgeClient looks up the sender → registrar pushes
mcp_message down the WS → extension answers → child discovers the tools.

Self-accepted LIVE (no LLM): integration-tests/cli/qwen-serve-client-mcp.test.ts
spawns a REAL qwen serve + REAL qwen --acp child under a mock OpenAI server,
a headless ws client registers + answers the MCP handshake, and the child
discovers the client-hosted chrome_read_page tool over the genuine
child→parent→WS channel (GET /workspace/mcp/chrome-tools/tools lists it).
Verified passing locally (24.6s). +5 bridge round-trip tests; 324 acp-bridge,
the 10 prior Phase-2, acpAgent 133, server+acp-http 675 all pass; builds clean.

Not exercised here: a real LLM turn driving tools/call (needs creds), and a
real Chrome extension as the WS client (headless ws stands in).

* fix(serve): session-scope runtime MCP servers so client tool calls resolve (#5626)

The reverse tool channel registered the client-hosted MCP server only on the
bootstrap/workspace Config, so discovery worked but a prompt — which runs
against an independent per-session Config from newSessionConfig→loadCliConfig
— couldn't resolve the tool ("not found in registry"), and the reverse WS
channel was never reached. Spec (docs/05) intends per-session scope.

Fix (minimal, additive, guarded; normal settings-based MCP servers unaffected):
- core/config.ts: add Config.getRuntimeMcpServers() (shallow copy of the
  private runtimeMcpServers map).
- acpAgent.ts newSessionConfig: copy the bootstrap Config's runtime MCP
  servers into a newly-created session Config before initialize() so its
  discovery binds that session's sendSdkMcpMessage (register-before-session).
- acpAgent.ts workspaceMcpRuntimeAdd/Remove: fan the add/remove out to every
  active session's McpClientManager (register-after-session), best-effort.

Test: the fake model now emits the fully-qualified registered name
mcp__chrome-tools__chrome_read_page (what a real model is handed); the
reverse channel still forwards the bare tool name to the client server.

Verified LIVE (no LLM, no creds): integration test now drives the FULL loop —
model→agent→session-registry resolution→reverse client_mcp/message over the
daemon WS→headless ws client returns CallToolResult→agent consumes it (tool
completed)→turn_complete. 2/2 integration tests pass (confirmed locally).
Regression: config 248, acpAgent 133, mcp-client-manager 101, client-mcp-ws 5
pass; builds clean. (server.test.ts has 2 pre-existing Web-Shell flakes,
identical on the unmodified baseline.)

Still needs a real Chrome extension (vs the headless ws stand-in) + a real
model turn for true browser behavior; the protocol round-trip is proven.

* chore(browser-ext): remove the dead Native Messaging stack (#5626)

Daemon-direct made Native Messaging obsolete — the extension talks to
`qwen serve` directly (chat over HTTP+SSE, browser tools over the daemon WS),
so the entire native-host stack is dead weight. Deletes ~15.5k lines:

- packages/mcp-chrome-integration/app/native-server/ — the whole native host
  (MCP servers, ACP client, Fastify server, doctor/register/report/postinstall,
  the superseded serve-agent backbone).
- extension background transport: native-messaging.ts, native-connection.ts,
  native-message-handler.ts, native-messaging-types.ts, ui-request-router.ts
  (+ its test). The still-used executor types (BrowserToolArgs / RawNetworkRequest
  / WebSocketSession / NetworkCaptureState) move to background/browser-tool-types.ts.
- service-worker rewired to daemon-direct only (drops the NativeMessaging
  init + the onMessage→routeUiRequest relay; keeps the browser-tools server start).
- esbuild.background.config.js drops the deleted native-messaging entry point.
- manifest.json drops the `nativeMessaging` permission.
- package.json scripts drop every native-server/native-host reference; dev-watch
  no longer spawns the native host.
- obsolete native-messaging docs (01-04) + scripts (diagnose/install/update) removed;
  README rewritten for daemon-direct.

Extension esbuild build green; tsc errors dropped (84 -> 69, all pre-existing
node:test/content typings). Daemon-side (cli/serve/core) untouched.

* chore(browser-ext): drop orphaned content-fetch-patch.ts (#5626)

* fix(serve): let browser extensions open the daemon WS reverse channel

The daemon-direct Chrome extension (#5626) connects to qwen serve's /acp
WebSocket to register its browser tools as a client-hosted MCP server.
Three gaps blocked the real-browser path. A node WS client in the
integration tests carries no browser Origin and completes the ACP
handshake, so none of these surfaced until a real Chrome connected:

- The WS CSRF check hard-coded loopback origins, so the extension's
  chrome-extension://<id> Origin was rejected with 403. Wire the
  existing --allow-origin allowlist into the WS upgrade check
  (acp-http/index.ts, server.ts) with the same match semantics as the
  REST allowOriginCors.
- parseAllowOriginPatterns rejected chrome-extension:// because its
  URL.origin is the opaque "null". Rebuild the canonical origin from
  scheme+host for opaque-origin schemes (auth.ts), with tests.
- The extension skipped the ACP initialize handshake and sent
  mcp_register directly, tripping the daemon's 30s initialize timeout.
  Send ACP initialize first and register only after the ack
  (browser-tools-server.ts).

Also allow console.* in the extension package (no stdio in the MV3
runtime) via the eslint no-console allowlist.

Verified end-to-end against a real Chrome: the daemon agent calls
mcp__chrome-tools__chrome_read_page and reads the live active tab.

* docs(browser-ext): Plan C (CDP tunnel) feasibility + implementation design

Assess routing chrome-devtools-mcp's ready-made DevTools toolset through the
extension's chrome.debugger to drive the user's real browser, instead of
re-implementing each tool in the extension (Plan A). Records, with source
verification against [email protected] + [email protected]:

- the createCDPSession wall (why zero-change reuse fails — Target.attachToTarget
  is "Not allowed" for chrome.debugger; cdp-mcp throws in McpContext.from);
- the patch-package fork shape (pin 1.4.0 + a ~2-site patch, not vendor/submodule);
- the daemon /cdp browser-level CDP emulation + sessionId routing design;
- minimal browser-level command set, reusable prior art (playwright-mcp
  --extension), phased steps, risks, and the Plan A fallback.

Refs #5626.

* feat(serve): add CDP browser-level emulator for Plan C tunnel (#5626)

First component of the Plan C "CDP tunnel": a synthesis layer that fakes the
browser-level CDP topology so an external puppeteer client (chrome-devtools-mcp)
can connect over a future /cdp endpoint while page-domain commands are forwarded
to the one real tab via the extension's chrome.debugger.

Implements the exact contract a Phase 0 spike proved necessary: a tab->page
two-level target tree + recursive Target.setAutoAttach (browser attaches the tab
session; the tab session attaches the page session), with page-session commands
routed to forwardToTab and tab events re-tagged with the page session id. The
spike connected real puppeteer to a pure synthesis layer and ran
page.evaluate(() => 1 + 1) === 2. 7 unit tests cover the handshake + routing.

Refs #5626.

* feat(serve): add CDP tunnel reverse-link, /cdp glue, and bridge registry (#5626)

Plan C Phase 1 daemon core. The reverse-link forwards page-domain CDP
commands to the extension over cdp_command/cdp_result frames (id-correlated,
timeout) and re-tags cdp_event onto the page session; cdp-ws wires a per-
puppeteer-connection emulator to the reverse-link bound to the single active
extension bridge in a process-scoped registry. The emulator gains setTabInfo
so the synthetic targetInfo reflects the real tab after cdp_attach.

* feat(serve): wire /cdp upgrade branch and cdpTunnelOverWs flag (#5626)

Adds the /cdp WebSocket upgrade branch to acp-http (reusing the loopback /
host-allowlist / auth / CSRF checks) and routes inbound cdp_* frames on the
extension's /acp socket to the bound reverse-link. The extension connection
registers as the active CDP bridge eagerly at ACP initialize so a /cdp
puppeteer client can bind immediately (avoids the attach chicken-and-egg).
Feature flag cdpTunnelOverWs is wired exactly like clientMcpOverWs
(env QWEN_SERVE_CDP_TUNNEL_OVER_WS=1, capability cdp_tunnel_over_ws),
DEFAULT OFF — existing behaviour is unchanged when off.

* feat(browser-ext): add CDP bridge over the reverse /acp socket (#5626)

The extension answers cdp_attach by attaching chrome.debugger to the active
tab and cdp_command via chrome.debugger.sendCommand, replying cdp_result;
chrome.debugger.onEvent -> cdp_event, onDetach -> cdp_detach. Reuses the
existing browser-tools-server /acp socket (routes cdp_* frames; tears the
bridge down on socket close) and mutually excludes with the
chrome_network_debugger_* tools (one debugger per tab).

* build(deps): pin chrome-devtools-mcp 1.4.0 + puppeteer-core 25.2.0, patch McpContext (#5626)

Pins the two CDP-tunnel client deps (exact, to keep the version-specific
patch and puppeteer's hardcoded ExtensionTransport topology stable) and adds
patches/chrome-devtools-mcp+1.4.0.patch. The patch wraps McpContext.#init's
devtoolsUniverseManager.init / serviceWorkerConsoleCollector.init in try/catch
so the createCDPSession wall (Target.attachToTarget -> -32000 over
chrome.debugger) no longer crashes the server on startup; only performance_* /
service-worker console degrade. Applied via the existing postinstall
patch-package hook (same form as patches/ink+7.0.3.patch).

* test(serve): add Plan C /cdp end-to-end acceptance harness (#5626)

Node script that starts the real daemon with the flag on, connects a mock
extension over /acp (ACP initialize + mcp_register, answering cdp_command with
page-domain CDP), then puppeteer.connect to /cdp and asserts
page.evaluate(() => 1 + 1) === 2 through the real daemon + emulator +
reverse-link. Allowlists the harness dir in eslint's node-script globals.

* fix(serve): gate CDP page commands behind attach completion (#5626)

Real-Chrome testing surfaced an ordering race the mock acceptance missed: the
extension's chrome.debugger.attach is async (it pops the debugger banner), but
forwardToTab forwarded page-domain commands immediately, so a fast puppeteer
Network.enable reached the extension before attachedTabId was set and failed
"CDP tunnel not attached to a tab". The mock extension acked attach
synchronously, which hid the race.

Add an attach gate in CdpReverseLink: forwardToTab awaits the in-flight
cdp_attach to settle (success or failure) before forwarding. Verified
end-to-end against REAL Chrome — puppeteer read the live active tab (a GitHub
PR page) through the tunnel: pages=1, real url/title/body returned.

Refs #5626.

* refactor(chrome-extension): delete Plan A side panel, content scripts, and reverse tool channel

Tears out the superseded Plan A surface so the extension can become a pure
CDP-tunnel pipe (chat moves to the daemon web UI, browser tooling runs as
chrome-devtools-mcp over the /cdp tunnel):

- src/sidepanel/ (side-panel chat UI)
- src/content/ (content scripts; the CDP tunnel drives DOM/Input via
  chrome.debugger, no injection needed)
- background reverse tool channel: browser-tools-server, browser-network-tools,
  network-capture-utils, browser-tool-executors, tool-catalog, tool-router,
  mcp-tool-result, browser-tool-types, and their tests
- public/sidepanel/sidepanel.html static asset

Refs #5626

* refactor(chrome-extension): rewrite service worker as minimal daemon CDP client

The service worker is now the entire extension logic: probe the daemon /health,
open the /acp WebSocket, send the ACP initialize handshake (the daemon closes
the socket on a 30s init timeout otherwise and binds this connection as the CDP
bridge at that point), then route cdp_* frames into the CDP bridge with capped
backoff reconnect. No more reverse MCP tool server (chrome-tools is gone).

cdp-bridge: drop the browser-network-tools import and the network-capture
mutual-exclusion branch in handleAttach (the network tools are deleted, nothing
to exclude); remove the now-unused isCdpTunnelAttached export.

Refs #5626

* build(chrome-extension): trim manifest, build config, and deps to the CDP pipe

manifest: drop content_scripts, side_panel, and the sidePanel/webRequest/cookies/
scripting/webNavigation permissions; keep only debugger/tabs/activeTab/storage
plus background, key, host_permissions, icons, and action.

build: background esbuild now has a single service-worker entry point (content
script gone); delete the UI esbuild/postcss/tailwind configs and drop the UI
build step + build:ui scripts; sync-extension no longer special-cases the gone
sidepanel assets; dev-watch no longer spawns the UI watcher.

deps: remove the side-panel-only deps (@qwen-code/webui, react, react-dom,
markdown-it, and the @types + the postcss/tailwind/autoprefixer CSS toolchain).

Refs #5626

* chore(chrome-extension): drop dead externally_connectable hook (#5626)

* test(serve): add real-Chrome /cdp local verification script (#5626)

* test(serve): add cdp-mcp-over-tunnel layer-C smoke check

* fix(extension): keep the CDP tunnel alive with chrome.alarms

MV3 service workers idle out after ~30s, so the tunnel silently dropped
whenever no puppeteer client was driving it and the user had to keep the
Service Worker DevTools open to hold the worker awake. Register a 30s
chrome.alarms keepalive: the recurring onAlarm dispatch holds the idle
timer off, and each wake of a terminated worker re-runs the top level to
reconnect.

* feat(serve): auto-register chrome-devtools-mcp over the CDP tunnel

Plan C (#5626) last mile: when `qwen serve` runs with the CDP-tunnel flag,
the agent should be able to drive the user's real browser. Rather than
hand-writing browser tools, auto-register the (patched) chrome-devtools-mcp
as a session MCP server pointed at this daemon's /cdp endpoint, so its 29
ready-made DevTools tools flow through the tunnel.

- run-qwen-serve: forward QWEN_SERVE_CDP_TUNNEL_OVER_WS + _PORT into the
  spawned ACP child via childEnvOverrides (same path as the MCP budget env).
- acpAgent: buildCdpTunnelMcpServer() injects the server into the
  top-precedence sessionMcpServers tier when the flag + port are present and
  the package resolves; trust left unset so tools default to 'ask' (no silent
  auto-approval of browser control); best-effort skip otherwise.

No settings.json edit and no hand-written tools required.

* fix(serve): gate CDP bridge registration to the extension (#5626)

Auto-registering every /acp initialize as the CDP bridge was last-writer-
wins. Once an ACP agent connects over the same /acp endpoint (web UI, Zed),
it would capture the bridge and receive cdp_* frames it can't answer,
stealing the tunnel from the extension. Gate registration on
clientInfo.name === 'qwen-cdp-bridge'; the extension (and the acceptance
mock) now identify themselves that way, while agent clients are left alone.

* feat(extension): open the web UI when the toolbar icon is clicked

The extension has no UI of its own (pure CDP-tunnel pipe; chat lives in the
daemon web UI), so clicking the toolbar icon did nothing after the side panel
was removed. Wire action.onClicked to open the daemon baseUrl in a new tab so
the icon is a useful entry point instead of a dead click.

* feat: host the web UI in a Chrome side panel (#5626)

The extension is a pure CDP-tunnel pipe with no UI of its own, so after the
side panel chat was removed the toolbar icon did nothing. Bring the side panel
back as a thin host that iframes the daemon web UI (chat + tools), so the
sidebar is the everyday entry point and reuses the web UI's pages/components
instead of shipping a second UI in the extension.

- extension: side_panel + sidePanel permission; sidepanel.html/js frames the
  daemon baseUrl; toolbar icon opens the panel (openPanelOnActionClick).
- daemon: the Web Shell sent frame-ancestors 'none' + X-Frame-Options: DENY,
  which blocked the iframe. Allow framing only for chrome-extension origins
  explicitly passed via --allow-origin; everything else still gets DENY.

* fix: prefer chrome-devtools over computer-use under the CDP tunnel + keep MV3 worker alive during attach (#5626)

Two issues surfaced driving the real agent:

1. The agent picked the OS-level computer-use tool (cua-driver) for browser
   tasks instead of the injected chrome-devtools-mcp — heavyweight screenshot/
   click loop that pegged a CPU and stalled turns. Disable computerUse when the
   CDP tunnel flag is on so browser automation goes through the tunnel.

2. The extension's MV3 service worker idled out *between* CDP commands (the
   agent pauses to think), detaching chrome.debugger and hanging the next
   command. Add a sub-30s keepalive while attached; the 30s alarm only covered
   idle reconnects, not in-flight attachments.

* chore(extension): stop tracking .extension-key.pem (signing private key)

The extension signing private key was committed and pushed — anyone with repo
access could impersonate the extension. Stop tracking it and gitignore *.pem.
Load-unpacked debugging needs only the public "key" in manifest.json, so this
doesn't affect dev on any machine; the .pem is kept locally for packaging.

* refactor(chrome-extension): flatten to packages/chrome-extension

Drop the mcp-chrome-integration wrapper + dead native-server (daemon-direct
no longer uses native messaging). The extension is now a top-level workspace
at packages/chrome-extension. Updated root workspaces, eslint globs, doc-path
comments, and the two node scripts' global directives. cli + extension builds
and eslint verified green.

* test(serve): assert cdp_tunnel_over_ws in the capability registry

The cdp_tunnel_over_ws capability was added to SERVE_CAPABILITY_REGISTRY +
CONDITIONAL_SERVE_FEATURES but the test's EXPECTED_REGISTERED_FEATURES and the
conditional drift-insurance branch weren't updated, failing 3 registry tests.
Add it to the expected list (after client_mcp_over_ws, matching registry order)
and add its assertion branch (predicate accepts/rejects the cdpTunnelOverWsEnabled
toggle).

* fix(serve): address review comments on CDP tunnel + client-MCP wiring (#5777)

- guard post-async ws.send() with readyState OPEN (daemon crash on extension
  disconnect, sendClientMcpAck + cdp endpoint sends)
- make client-mcp-sender-registry delete() ownership-aware (cross-connection
  server-name collision)
- type the __clientMcpOverWs runtime config flag carrier
- document the CDP tunnel trust model (loopback + bridge-gated dumb pipe)

* chore(cdp-tunnel): set copyright year to 2026 on files created this year

The Plan C / #5626 files were authored in 2026 but carried a 2025 header.
service-worker.ts (created last year under #1432) keeps 2025.

* feat(chrome-ext): add side-panel onboarding gate + fix packaging

The side panel framed the daemon Web Shell unconditionally and showed a
static "Connecting…" line. When no daemon was reachable, or the daemon
wasn't started with --allow-origin (so frame-ancestors blocks the iframe),
the user was stuck on "Connecting…" with no guidance, and discovery.ts's
health check was never wired in.

Wire a health/capabilities gate into the panel:
- probe GET /health, then GET /capabilities
- down                         → "Start qwen serve" + the exact command
- up but no `allow_origin` feat → "Allow this extension" + the command
- ready                        → frame the Web Shell

The command is built from chrome.runtime.id at runtime, so it always names
this extension's real origin (dev-unpacked or published) — no need to know
or hardcode the id, and no publish-first chicken-and-egg. The pure decision
helpers live in onboarding-logic.js.

Also:
- fix `npm run package` so manifest.json sits at the zip root (the old
  `zip ... extension/` nested it under extension/, which the Chrome Web
  Store rejects)
- gitignore that packaging zip; add a package README

Part of #5626. PR #5777.

* docs(cdp-tunnel): trim over-long Plan C comments (ponytail)

Comment-only: compress multi-paragraph design rationale to intent, keep the
non-obvious why (trust model, bridge gate, attach ordering) + ponytail
ceilings. ~-91 lines, no code touched, build + tests green.

* fix(serve): address second-round review comments on the CDP tunnel (#5777)

- close the bound /cdp puppeteer socket on extension disconnect (was hanging
  ~170s on CDP timeout) via an onExtensionGone hook
- reject a 2nd concurrent /cdp client instead of silently clobbering routeInbound
- narrow extension host_permissions from <all_urls> to localhost (chrome.debugger
  needs no host perm; only the /health fetch needs localhost)
- log safeWsSend drops under serve debug mode so a dead tunnel is diagnosable

* feat(chrome-ext): polish side-panel welcome into a terminal console

Replace the bare welcome with a console that matches the product (a CLI
daemon): the command is the hero, typed at a `$` prompt with a blinking
cursor inside a titled terminal card. Warm-charcoal / electric-lime,
light + dark aware (prefers-color-scheme), staggered load-in, a pulsing
"listening" status, and a reduced-motion guard. No web fonts / no inline
JS (the extension CSP allows neither).

No behavior change: same /health + /capabilities gate and the
chrome.runtime.id-derived command. Mechanics tidied alongside the markup:
visibility toggles a .hidden class (CSS owns the flex layout), the copy
button updates a label span, and the command is prefilled synchronously
so first paint isn't an empty prompt.

PR #5777.

* feat(chrome-ext): click-to-copy command + centered copy button

Make the onboarding command easier to grab: the whole command row is now
click-to-copy (keyboard-reachable, Enter/Space), and a centered "Copy
command" button sits at the foot of the terminal card. Both flash a
check-mark "Copied" confirmation; the small top-bar button is gone.

No gate-logic change. PR #5777.

* test(serve): cover CDP-tunnel + client-MCP regression guards

Add the four focused unit tests flagged in review for load-bearing
reverse-channel paths that had no coverage:

- ClientMcpSenderRegistry: ownership-scoped delete — a disconnecting
  connection must not remove an entry a peer re-registered under the
  same name.
- CdpTunnelRegistry: register/supersede/unregister lifecycle, inbound
  routing delegation, onExtensionGone-on-disconnect, and the
  stale-unregister guard that must not evict a newer active bridge.
- CdpReverseLink: a forwarded command rejects when its per-command
  timer expires (not just on bulk dispose).
- safeWsSend: drops (no send, no throw) on a CLOSED/CLOSING socket.

safeWsSend is extracted from acp-http/index.ts into its own module so
it's unit-testable in isolation; behavior unchanged.

PR #5777.

* fix(serve): address bot code-review findings on the CDP tunnel (#5777)

- cdp-bridge: tear down listeners before re-attach (was double-registering →
  duplicate cdp_event frames corrupting puppeteer)
- emulator: return a CDP error for an unknown session instead of fake success
- registry: notify the superseded bridge so the old /cdp closes (single-puppeteer)
- deps: move chrome-devtools-mcp + puppeteer-core to optionalDependencies (~26MB)
- tests: entry-script validation, deliverClientMcpMessage error branches,
  registry supersede

* fix(chrome-ext): revert side panel to welcome when the daemon stops

Once the panel framed the Web Shell it stopped probing, so if the daemon
later went away the iframe was left showing Chrome's localhost
connection-refused page with no way back. Keep probing after framing and,
after a short tolerance (2 misses, ~5s, so a transient blip doesn't nuke a
live chat), clear the iframe src and show the welcome screen again.

PR #5777.

* fix(chrome-ext,serve): address review comments on the CDP tunnel

- service-worker: redact the bearer token from the connect log, and guard
  connect() against a still-CONNECTING socket so a rapid reconnect can't
  orphan an in-flight handshake.
- acpAgent: don't clobber a user-configured `chrome-devtools` MCP server
  with the tunnel auto-wire.
- cdp-reverse-link: log dropped/unexpected inbound frames via an optional
  diagnostic sink instead of swallowing them silently.
- name the cross-package `qwen-cdp-bridge` client-name constant on both
  sides instead of repeating the bare string.

PR #5777.

* refactor(chrome-ext): inline onboarding helpers, drop dead pollTimer

onboarding-logic.js was a 65-line file (mostly JSDoc) for three trivial
helpers and a constant, split out "for testability" that was never used.
Fold them into sidepanel.js (decideState collapses into probeState's
return; resolveBaseUrl/allowOriginCommand become a one-liner each) and
delete the file + its import.

Also drop `pollTimer`: the welcome-fallback change made it write-only (the
only clearInterval was removed), which trips no-unused-vars.

PR #5777.

* fix(serve,chrome-ext): more CDP-tunnel review fixes

- sidepanel: pass the bearer token through the Web Shell URL fragment so a
  token-gated daemon doesn't 401 every framed request.
- server: throw if deps.bridge is injected without deps.clientMcpSenderRegistry
  (the bridge is already wired to its own sender; a fresh one would be orphaned).
- cdp-browser-emulator: surface unhandled browser-level CDP commands via an
  optional log sink (keep the empty-result ack, with a TODO).
- cdp-bridge: only treat "already attached" as ours when attachedTabId === tabId
  (a foreign DevTools owner now errors), and detach the previous tab on switch
  so Chrome drops its debug banner.
- build.js: add packages/chrome-extension to the build order so root build
  exercises the extension bundle.

PR #5777.

* fix(serve,chrome-ext): harden CDP-tunnel + client-MCP reverse channel

- client-mcp-ws: cap registered servers per connection (max 10) and re-check
  `disposed` after the provider round-trip so a WS close mid-register can't
  leave a zombie server; add registrar serverCount().
- acp-http: rate-limit client-MCP frames (mcp_register/unregister at the
  mutation tier, mcp_message at read) and cap concurrent fire-and-forget
  register/unregister dispatch (max 8) to stop DoS amplification.
- cdp tunnel: on /cdp puppeteer disconnect send a `cdp_release` frame so the
  extension detaches chrome.debugger instead of leaving the tab's debug banner
  up until /acp dies.
- acpAgent: skip chrome-devtools auto-registration (with a diagnostic) when the
  /cdp tunnel requires bearer auth — the ACP child can't authenticate to it.

PR #5777.

* fix(chrome-ext,serve): address second-round CDP-tunnel review

- service-worker: only the *active* socket's close tears down the bridge — a
  stale daemon-forced close must not detach the new connection's debugger.
- daemon config + sidepanel: fail closed on a non-loopback baseUrl so a tampered
  chrome.storage value can't exfiltrate the bearer token off-host (background
  fetch/WS bypass host_permissions).
- sidepanel: reentrancy guard on tick() so overlapping slow probes don't burn
  the framed-miss tolerance and flash the welcome screen mid-chat.
- cdp-bridge: reentrancy guard on handleAttach so overlapping cdp_attach frames
  can't interleave teardown and corrupt attachedTabId.
- add cdp-ws.test.ts: regression cover for no-bridge reject, second-client
  reject, onExtensionGone fail-fast, cdp_release on dispose, and the
  superseded-bridge cleanup guard.

PR #5777.

* fix(serve,webui): address third-round CDP-tunnel review

- safe-ws-send: wrap the debug-mode writeStderrLine in try/catch so a broken
  stderr (EPIPE on a piped/closed log) can't break the "never throw on a dead
  socket" contract production callers rely on.
- ChromeToolCall: index-access rawInput['name'] to satisfy
  noPropertyAccessFromIndexSignature.

PR #5777.

* fix(serve,chrome-ext): address #5777 review round 4

- acp-http: send mcp_error back on an unexpected client-MCP handler rejection
  (register/unregister callers otherwise hang); log when the inflight cap rejects.
- cdp-ws: log the happy-path cdp_release dispatch for oncall tracing.
- run-qwen-serve/acpAgent: don't pass a bogus "0" CDP-tunnel port for ephemeral
  --port 0, and emit a stderr diagnostic when the tunnel is disabled for a
  missing/invalid port instead of failing silently.
- cdp-bridge: handle a cdp_release that races an in-flight handleAttach so a late
  attach can't leave a debugger attachment with no live /cdp client.
- service-worker: close the WS on an ACP initialize error so the daemon doesn't
  keep holding a non-functional CDP bridge.

* fix(serve,chrome-ext): address pr-review findings (#5777)

- client-mcp-sender-registry: gate the child-side runtime-server teardown on
  ownership too (Config.removeRuntimeMcpServer is not owner-scoped), so a
  disconnecting connection can't kill a server a later connection re-registered
  under the same name. (P2)
- service-worker: carry the bearer token via the `qwen-bearer.*` WS subprotocol
  (matching the web-shell + daemon decoder) instead of a `?token=` query the
  daemon never reads, so a token-gated daemon no longer 401-reconnect-loops. (P3)
- run-qwen-serve: advertise client_mcp_over_ws / cdp_tunnel_over_ws in the
  bootstrap /capabilities too, matching the runtime path. (P3)

* chore(webui): remove unused ChromeToolCall component (#5777)

ChromeToolCall was added in a debugging commit but is never wired into the
tool-call routing (getToolCallComponent) — there's no `chrome` tool kind and
the only references were barrel re-exports. Dead code; remove it.

* fix(serve,chrome-ext): address #5777 review round 5 (diagnostics)

- service-worker: log the WS close code/reason so failure modes aren't
  indistinguishable (e.g. the daemon's 1011 "no extension connected").
- acpAgent: containment-check the resolved chrome-devtools-mcp bin path so a
  malformed `bin` field can't escape the package dir.
- cdp-tunnel-registry: log when a new extension bridge supersedes a stale one.

* fix(chrome-ext,serve): address #5777 review round 6

- cdp-bridge: ack the attach (as an error) before tearing down on a
  release-during-attach, so the daemon's reverse link doesn't hang ~170s
  waiting for a cdp_attached that never arrives.
- acp-http: rename isFireAndForget -> dispatchOffQueue + clarify the comment;
  register/unregister are dispatched off-queue but still expect a response ack,
  so the name no longer reads as "no response."

* fix(serve,cli): address #5777 review round 7

- config: warn (stderr) when QWEN_SERVE_CDP_TUNNEL_OVER_WS overrides an explicit
  tools.computerUse.enabled=true, so the effective config isn't a silent surprise.
- client-mcp-ws: document the intentional idempotency of handleUnregister.
- cdp-reverse-link: add tests for the attach gate — forwardToTab parks behind an
  in-flight attach, the cdp_attach timer rejects, and the gate opens on timeout
  so commands don't hang.

* fix(serve,core): address #5777 review round 8

- mcp-client: only treat the first stdio arg as a local entry script when
  it is clearly a filesystem path. A bare includes('/') also matched scoped
  npm package names (npx @scope/pkg), wrongly resolving them under the
  workspace and throwing before the runner ran. Adds a regression test.
- client-mcp-sender-registry: reject shadowedSettings so a browser-hosted
  WS client cannot shadow a user-configured MCP server name; roll back the
  child-side add.
- cdp-ws: close the puppeteer socket when /cdp attach fails so dispose()
  clears cdpBound/routeInbound, instead of a stuck tunnel until restart.

* fix(serve,chrome-ext): address #5777 review follow-ups

* fix(serve): satisfy CDP inbound frame guard typing

* fix(serve): address CDP tunnel review feedback

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-06-28 15:57:31 +00:00
673454839e feat(memory): add a git-shared team memory tier (#5886)
* feat(memory): add a git-shared team memory tier

Add an opt-in third auto-memory tier — TEAM — stored in the repository at
`.qwen/team-memory/` and shared with collaborators through git. The private
USER and PROJECT tiers are unchanged.

- Enabled via the `memory.enableTeamMemory` setting (default off) or the
  `QWEN_CODE_MEMORY_TEAM` env override; inactive otherwise.
- The model is taught a three-tier prompt and when to route saves to the
  shared tier (project-wide conventions, team-wide references).
- Writes to the team directory are scanned for secrets (a curated
  gitleaks-style ruleset) and hard-blocked, since the directory is committed
  to the repo and visible to everyone with repo access.
- Team writes default to 'ask' permission — not auto-allowed like the private
  tiers — so they are proposed for confirmation in the default approval mode
  and surface in the git diff for review.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(vscode): update settings schema for team memory

* fix(memory): harden team memory review gaps

* fix(memory): address team-memory PR review feedback

- secret-scanner: AWS key suffix is base62 ([A-Z0-9]), not base32
  ([A-Z2-7]) — old class missed IDs with digits 0/1/8/9; add a
  positive test for such a key.
- paths: document that isAnyAutoMemPath deliberately excludes team
  memory (security-load-bearing — team writes must stay 'ask').
- extractionAgentPlanner: name the team-memory dir in the scoped deny
  message so a denied team write is debuggable.
- prompt: spell the tier count via a number-word lookup so a future
  4th tier never silently reads "two".
- team-memory-secret-guard: debug-log matched rule IDs only (never the
  secret content) when a write is blocked.
- edit: when a blocked team write's secret already exists in the
  on-disk file, tell the user to delete the committed secret.
- docs: caveat that a directory-form gitignore (.qwen/) makes the
  !-reinclude a no-op; use the file-glob form.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): harden team-memory secret guard on blocked writes and notebooks

- write-file: blocked team-memory secret write now returns an `error`
  field (INVALID_TOOL_PARAMS) so the framework treats it as a failure
  instead of a silent success, mirroring edit.ts.
- notebook-edit: scan the serialized .ipynb that hits disk through the
  team-memory secret guard before writing, blocking with the same
  `error` field; behavior unchanged for non-team-memory paths.
- store: log non-ENOENT errors in readTeamAutoMemoryIndex before
  re-throwing, so EACCES/EIO/ELOOP no longer vanish via config.ts's
  best-effort `.catch(() => null)`.
- config: emit a debug diagnostic when team memory is enabled but
  suppressed by the untrusted-workspace trust gate.

Co-Authored-By: Qwen-Coder <[email protected]>

* feat(memory): auto-generate the team memory index

Generate the team `MEMORY.md` index by scanning the saved memory files
instead of having the model hand-maintain it. The index is derived and
ordered by path, so the committed file is deterministic across machines.
This removes the git merge-conflict surface a hand-edited shared index would
have, keeps the index consistent with the files, and frees the model from the
two-step save for the team tier.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(memory): opt-in git sync for team memory

Add an opt-in, best-effort git sync of team memory at session start so a
daemon can share team memory through the remote without manual git. Off by
default; enabled with `QWEN_CODE_MEMORY_TEAM_SYNC=1`.

It commits the team directory (only that path), pulls fast-forward-only, then
pushes. `--ff-only` is deliberate: it never creates a merge commit or a
conflict — a diverged branch is skipped rather than touching the working tree.
All git is best-effort and never throws, so a failure cannot break a session.
Commits can be attributed to the acting user via an optional author, for
cooperative per-user attribution on a shared daemon.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): close team-memory secret-guard regex + test gaps

- secret-scanner: add `_` to the two T3BlbkFJ-marker char classes so legacy
  base64url OpenAI keys with underscores in the body no longer bypass the
  scanner; extend the OpenAI positive samples with an underscore key body.
- edit: cover the pre-existing-secret branch — seed the on-disk file with a
  full detectable token, assert the distinct "already exists" message and that
  the blocked edit leaves the file untouched.
- team-paths: cover the first-ever write before .qwen/team-memory exists,
  exercising realpathNearestExisting walking up to an existing ancestor.

Co-Authored-By: Qwen-Coder <[email protected]>

* feat(core): add Google OAuth client secret and SendGrid secret-scanner rules

Extend the team-memory secret scanner with two more high-confidence,
distinctive-prefix credential patterns (both gitleaks rules):

- gcp-oauth-client-secret: GOCSPX- prefix + 24 base64url chars,
  placed next to the existing GCP AIza API-key rule.
- sendgrid-api-token: SG. + 22-char id + . + 43-char secret.

Both have a near-zero false-positive bar, matching the curated set's
existing criteria. Add a load-bearing positive sample per rule.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): warn when team memory is enabled but not git-shareable

When the team tier is active, emit a one-time startup warning if its
directory is git-ignored (e.g. a directory-form `.qwen/` ignore that a
`!`-reinclude cannot escape) or there is no git root — so the tier never
silently shares nothing. Gated on the active (trusted + enabled) tier;
probes a representative file path with `git check-ignore`, and is
latched so refreshHierarchicalMemory re-runs do not re-emit it.

Closes the last open review thread (wenshao + qwen-code-ci-bot both
flagged the silent-inert case); the docs caveat already shipped.

Co-Authored-By: Qwen-Coder <[email protected]>

* feat(memory): dedup team index by description; fix empty-frontmatter parse

The team index now collapses entries that share a (normalized) description
into one line — listing the other files via "(also: …)" — so two people
saving the same shared fact don't surface as duplicates. Nothing is dropped;
every file path is preserved and the grouping is deterministic (input is
path-sorted).

Also fix a pre-existing frontmatter bug: parseFrontmatterValue used `\s*`,
which crosses newlines, so an empty value (`description:`) captured the next
frontmatter line. Bound it to horizontal whitespace.

Co-Authored-By: Qwen-Coder <[email protected]>

* feat(memory): add memory.enableTeamMemorySync setting

Team-memory git sync was env-only (QWEN_CODE_MEMORY_TEAM_SYNC). Add a
first-class `memory.enableTeamMemorySync` setting mirroring the
enableTeamMemory plumbing (settingsSchema, cli/core config, acpAgent
type/schema/default/keys/normalizer, desktop mirror), with the env var kept
as a 0/1 override. Off by default. Docs updated.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): audit hardening — resilient scan, deterministic team index

From a multi-round adversarial audit of the team-memory tier:

- scan: a single unreadable memory file (EACCES, or a TOCTOU delete during
  `git pull`) no longer rejects the whole scan and wipe every memory from the
  index — per-file failures are caught and skipped (debug-logged).
- indexer: order the team index by code unit, not `localeCompare()`, so the
  committed MEMORY.md is byte-identical across machines/locales and the
  ff-only sync can't wedge on index churn.
- scan: escape the frontmatter key before building its RegExp (latent hardening).
- config: surface a skipped/failed team-memory sync at debug level instead of
  silently swallowing it.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): deterministic team index cap + surface diverged sync

Round-2 audit follow-ups:

- scan: the >200-file cap selected its subset by mtime + locale basename, which
  differs per machine/locale — so a large team's committed index churned and
  wedged the ff-only sync. Team scans now cap by code-unit path (deterministic);
  private tiers keep mtime-recency (never shared, so it's fine).
- sync: a diverged branch made `pull --ff-only` (and the push) fail with no
  `skippedReason` — a silent no-op for a user who opted into sync. Now reported
  as `pull-failed` / `push-failed` and logged. Docs note pull/push are
  whole-branch, not path-scoped.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): regenerate settings schema for enableTeamMemorySync

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): harden team-memory git sync (reconcile-first, scoped push, safe failure)

The opt-in team-memory git sync could publish unrelated local commits, wedge a
two-writer branch, leave team files staged on commit failure, and hang session
start on a credential prompt. Harden all four maintainer-flagged criticals:

- Reconcile first: run `git pull --ff-only` BEFORE committing so the local team
  commit lands on top of upstream instead of diverging; a diverged branch is
  skipped cleanly as `pull-failed` rather than committing then wedging `--ff-only`.
- Scoped push: push only when THIS sync created the commit and the branch was
  not already ahead of `@{u}` (pre-sync), via an explicit single-branch refspec
  (`HEAD:<merge-ref>`) — never an unqualified `git push`. New `local-ahead` skip.
- Safe failure: unstage the team path when the commit fails (hook/GPG/identity)
  so a user's next manual `git commit` cannot sweep team files in.
- Non-interactive git: force `GIT_TERMINAL_PROMPT=0` + batch-mode SSH, SIGKILL on
  timeout, and a shorter timeout so sync cannot block session start on a prompt.

config refresh: rebuild the team index BEFORE syncing (so a fresh MEMORY.md is
committed/pushed, with a re-build after a pull that brought new files) and key
the shareability latch by projectRoot so a newly entered repo is re-checked.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): harden team-memory index/scan/path security

Address the security/correctness criticals from PR #5886 review. Team
memory is committed to the repo and loaded into every collaborator's
system prompt, so its write/scan/classification paths are trust
boundaries.

- indexer: pass noFollow on the team MEMORY.md write and reject a
  symlinked team root, so a committed `.qwen/team-memory` (or index)
  symlink can't redirect the generated index outside the repo.
- indexer: sanitize attacker-controlled frontmatter title/description
  (strip control/zero-width/bidi chars, collapse newlines, defang
  code/link markdown, cap length) before embedding into MEMORY.md, to
  prevent prompt injection into the shared context.
- indexer: skip the rewrite when the generated index is byte-identical,
  avoiding mtime churn and no-op commit cycles.
- scan: normalize CRLF before parsing so a Windows checkout's team files
  don't silently drop out of the shared index.
- paths: resolve a dangling final-component symlink via lstat/readlink in
  realpathNearestExisting, so a `decoy -> team-memory/leak.md` (missing
  target) is classified as team and the secret scanner can't be bypassed.
- secret-scanner: bound both quantifiers in the openai-api-key T3BlbkFJ
  rule ({20,512}) to remove O(n^2) ReDoS backtracking; real keys still
  match.
- team-memory-git-status: also probe a representative topic path so a
  repo that re-includes the index but ignores topic files is flagged as
  non-shareable.
- notebook-edit: run the team-memory secret scan in validateToolParamValues
  too, for parity with edit/write-file.

Adds load-bearing tests for each fix.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): sanitize team index paths and verify whole-path containment

Refines the earlier team-memory security hardening with two fixes the
first pass left open:

- sanitizeIndexPath now defangs the attacker-controlled relativePath
  rendered into the committed MEMORY.md — at BOTH the `](path)` link
  target and the "(also: ...)" dedup suffix. A git filename may legally
  contain newlines and markdown delimiters, so a raw path could inject a
  second physical line (e.g. "- SYSTEM:") and reopen the prompt-injection
  structure the patch was closing for title/description.
- rebuildTeamAutoMemoryIndex now realpath-resolves the whole team root
  and requires it to equal repoRoot/.qwen/team-memory. The prior lstat
  only inspected the leaf, so a symlinked PARENT component (e.g.
  `.qwen -> /tmp/out`) let scan/write escape the repo undetected while
  the guard believed it had rejected symlinks.

Also adds a regression test pinning the security-load-bearing invariant
that isAnyAutoMemPath excludes team paths (so team writes stay ask-gated).

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): keep team index link targets addressable, not just safe

The earlier injection fix rewrote legal filename chars (`(` `)` `[` `]` and
backticks) to `_` and used that destroyed value as the ACTUAL MEMORY.md link
target. A real file `feedback/a(b).md` was emitted as `](feedback/a_b_.md)`,
which does not exist (or, worse, collides with a different real file) — the
link was injection-safe but no longer addressable.

Replace `sanitizeIndexPath` with `encodeIndexPathTarget`, a reversible
percent-encoder: every char outside an addressable allowlist (alphanumerics
and `/ . - _ ~`) is percent-encoded, so breakout chars become inert ASCII
(newline->%0A, `(`->%28, `)`->%29, space->%20, backtick->%60, ...). The target
is a single line with no `](`/`)` breakout, yet `decodeURIComponent` recovers
the exact original path so the link resolves to the real file. `/` is kept
literal so the path still works as a relative reference. Applied at both index
sites (the main `](path)` link and the `(also: ...)` suffix). The display label
(title) stays non-reversibly sanitized as before — it is not addressable.

Update the path tests to assert the emitted target percent-decodes back to the
real relative path, and add a `feedback/a(b).md` case. The newline+`]`/`)`
injection assertions remain (mutation-verified: they fail if the encoding is
dropped).

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): gate team-memory sync on the index safety check

rebuildTeamAutoMemoryIndex throws when the team root is a symlink that
could redirect the committed index outside the repo, but the surrounding
.catch swallowed the error and syncTeamMemory ran independently — so a
refused symlink/escape still got git add/commit/push'd on the out-of-repo
dir, defeating the indexer's refusal. Capture whether the rebuild
succeeded and only sync when it did, so a failed safety check skips sync
entirely.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): harden team-memory git sync and cover execute-time secret scan

Several robustness fixes to the team-memory git sync, plus a missing test:

- Respect a user's GIT_SSH_COMMAND: append our non-interactive batch guards
  (-oBatchMode=yes -oConnectTimeout=5) onto it instead of clobbering it, so a
  custom identity (-i), proxy jump (-J), or non-standard port is preserved.
- Skip cleanly on a detached HEAD (new `detached-head` skippedReason) instead
  of creating an orphaned commit that can never be pushed. A branch with no
  upstream still commits locally as before.
- Use SIGTERM (not SIGKILL) as the git kill signal so a timeout mid-commit/-pull
  lets git release index.lock and clean up; the non-interactive ssh guards
  already bound the network-hang case.
- docs(memory): correct the sync description to match the implementation
  (rebuild index, ff-pull BEFORE commit, push only the scoped sync commit,
  skip on divergence/no-upstream).
- test(notebook-edit): cover the execute-time team-memory secret scan (the
  full-notebook backstop), distinct from the existing validate-time test.

Co-Authored-By: Qwen-Coder <[email protected]>

* fix(memory): split security vs operational team-sync failures; per-op git kill signal

Refine the recent team-memory sync changes per review:

- indexer: throw a typed TeamMemoryRootSecurityError for the symlink/escape
  rejection so the sync gate can tell a SECURITY refusal apart from an
  OPERATIONAL IO failure. config now blocks sync ONLY on the security class;
  an operational rebuild failure (EACCES/ENOSPC/EPERM) is logged but
  self-corrects on the next rebuild and no longer permanently gates legitimate
  team sync. The security invariant (never add/commit/push an escaping root)
  is unchanged.
- team-memory-sync: pass a per-op kill signal to tryGit — SIGKILL for the
  hang-prone network/ref ops (pull/push/rev-parse/symbolic-ref; no index.lock
  to corrupt), SIGTERM for mutating ops (add/commit) so git can release locks
  and clean up. Node's execFile timeout does not escalate to SIGKILL on its
  own, so a child trapping SIGTERM could hang past the timeout.
- team-memory-sync: thread the resolved branch into resolvePushTarget so it no
  longer re-spawns `git symbolic-ref` after the detached-HEAD check.
- docs: fix a broken sentence in the team-memory git-sync section.
- tests: cover both rebuild failure classes (security -> sync skipped;
  operational -> sync still runs) and a positive gate (rebuild ok + sync
  enabled -> sync runs); assert the typed error on the indexer symlink tests.

Co-Authored-By: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-06-27 12:29:48 +00:00
顾盼andShaojin Wen 44b80da0db feat(memory): confirm auto-generated skills before persisting (#5616)
* feat(memory): add memory.autoSkillConfirm setting schema

* feat(memory): add Config.getAutoSkillConfirmEnabled()

* feat(memory): wire memory.autoSkillConfirm through cli/acp/desktop settings

* feat(memory): add pending-skills staging helpers

* feat(memory): stage auto-skills for confirmation in runSkillReview

* feat(memory): pass autoSkillConfirm flag from client to skill review

* feat(memory): skill-review subscriptions + accept/reject pending APIs

* feat(cli): add skill-review dialog state to UI context

* feat(cli): add SkillReviewDialog component

* feat(cli): render SkillReviewDialog from DialogManager

* feat(cli): wire skill-review subscription and idle dialog routing

* feat(cli): show pending auto-skill review hint in footer

* feat(cli): add autoSkillConfirm toggle to /memory dialog

* docs(memory): document memory.autoSkillConfirm setting

* fix(cli): focus and Ctrl+C-close the skill-review dialog

* fix(memory): address review on auto-skill confirmation

- stage only newly-created skills, never agent-edited pre-existing ones, so
  Discard can't delete a skill the user already confirmed
- re-read pendingSkills after the await in resolvePendingSkill so concurrent
  Keep-all/Discard-all removes every entry, not just the last
- surface accept/reject fs failures (try/catch + log + .catch) instead of
  silently swallowing them
- remount SkillReviewDialog per task via key so its snapshot never goes stale
  across consecutive skill-review batches
- skip redundant skillReviewPending updates with a signature compare
- remove the unreachable openSkillReviewDialog action
- add debug logging to the pending-skills module
- ignore .qwen/pending-skills/ explicitly in .gitignore

* fix(memory): address round 2 review on auto-skill confirmation

- acceptPendingSkill: when the staged dir is gone, no-op only if the skill is
  already in the skills root; otherwise throw so resolvePendingSkill keeps it
  pending and logs, preventing silent data loss
- fall back to the agent's systemMessage for progress text when staging yields
  zero pending (a pre-existing-skill edit is still a durable change)
- log the no-task / no-target early returns in resolvePendingSkill
- replace internal tracker references in an AppContainer comment

* fix(memory): harden auto-skill confirmation for multi-batch and edge cases

- parseDescription: keep an empty description empty instead of spilling onto
  the next YAML line
- namespace staged dirs under the task id so a later same-named batch can't
  clobber a still-deferred earlier one
- track Esc-dismissed batches in a Set (not a single value) and only mark a
  batch dismissed on Esc, so a partially-failed Keep-all can reopen for the
  unresolved skills
- document the in-place updateRecord invariant the accept/reject race fix
  relies on
- add the missing license header to pending-skills.test.ts

* fix(memory): strip quoted descriptions; Ctrl+C defers skill-review dialog

- parseDescription: strip a matching pair of surrounding quotes so a
  `description: "..."` frontmatter value isn't rendered with literal quotes
- useDialogClose: Ctrl+C on the skill-review dialog now calls
  dismissSkillReviewDialog (records the batch as dismissed) instead of plain
  close, matching Esc — otherwise the idle effect immediately reopened it

---------

Co-authored-by: Shaojin Wen <[email protected]>
2026-06-25 00:47:16 +00:00
qqqysandQwen-Coder d360dd276e feat(serve): voice dictation over the daemon for the Web Shell (#5755)
* feat(serve): voice dictation over the daemon for the Web Shell

Bring voice input to the `qwen serve` Web Shell. The browser captures the
microphone, streams raw 16 kHz mono PCM to a new `/voice/stream` WebSocket, and
the daemon transcribes server-side by reusing the existing CLI voice pipeline
(realtime streaming + on-stop batch) — provider credentials never reach the
browser, and it works whether the daemon is local or remote.

Daemon:
- `/voice/stream` WebSocket handler (serve/voice) reusing openVoiceStream /
  openQwenAsrRealtimeStream / transcribeVoiceAudio; resolves the workspace
  voiceModel from a ModelsConfig built off settings.
- Routed through the existing ACP upgrade listener so it shares the
  loopback / host-allowlist / CSRF / bearer checks; concurrency-capped.
- Advertises the `voice_transcribe` capability (unconditional, like
  auth_device_flow; the WS errors when no voice model is configured).
- Relax `Permissions-Policy` to `microphone=(self)` so the same-origin shell can
  request the mic — an empty `microphone=()` allowlist blocked the prompt
  entirely.
- Allowlist `voiceModel` on `/workspace/settings` so the picker can read it.

Web Shell:
- Mic button in the composer: click to record, click to stop -> the transcript
  is inserted into the input for review before sending.
- `/model --voice` picker (sourced from `/workspace/providers`, since voice
  models are hidden from the session model list) and `/model --voice <id>`,
  persisted via the prompt channel like `/model --fast`.

The voiceModel resolver now accepts a structural model lookup so the daemon can
resolve it without constructing a full CLI Config.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(serve): harden web voice streaming

* fix(serve): clean stale voice capture resources

* fix(serve): harden voice websocket errors

* fix(serve): hide voice capability when token auth is configured

* fix(serve): address voice review blockers

* fix(web-shell): keep voice cancel available when disabled

* fix(web-shell): inline the voice mic in the composer toolbar

The mic was an absolute-positioned overlay anchored to the composer
wrapper, so after the #5775 chat-UI restructure it floated outside the
input box (clipped at the bottom-right). Render it inside ChatEditor's
`toolbarRight`, just before the send button, wiring the transcript insert
to the composer core — it sits inline with the `/`, `@`, and send
controls like the rest of the toolbar.

Restyle to a Codex-style recording experience: an idle mic that matches
the other toolbar icon buttons, and a recording pill with a live waveform
(driven by the RMS meter), an elapsed `M:SS` timer, and a stop control.
Also forward `/voice/stream` through the vite dev proxy (`ws: true`,
scoped to the exact path so it doesn't shadow the client's own
`client/voice/*` source modules) so the WebSocket reaches the daemon in
dev instead of silently failing against vite.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(serve): authenticate the voice WebSocket via a bearer subprotocol

Voice was suppressed on any token-configured daemon because browsers
cannot set an `Authorization` header on a WebSocket, so the bearer check
on the shared ACP upgrade listener always failed for the browser mic.

Let the browser carry the token in `Sec-WebSocket-Protocol` as
`qwen-bearer.<base64url(token)>` (the only header a browser can set on a
WS). The upgrade listener now accepts the token from either the
`Authorization` header (non-browser clients, unchanged) or the
subprotocol, hash-compared in constant time. `handleProtocols` selects a
non-secret subprotocol (or none) so the token is never echoed back in the
handshake response. This aligns the voice WS with how the ACP WS will
authenticate browsers.

With the WS now authenticated, drop the `!tokenConfigured` /
`requireAuth` suppression of the `voice_transcribe` capability — it is
advertised whenever the `/voice/stream` endpoint exists. Tests updated to
assert voice is advertised under a token / `--require-auth`.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(serve): make the voice WS subprotocol handshake complete + test it

Review follow-up to the bearer-subprotocol auth. Fixes a real handshake
bug and closes the test gap on the security-critical path.

- Handshake bug: when the browser offered only the `qwen-bearer.*`
  subprotocol, the daemon's `handleProtocols` selected none, and a strict
  WS client (the `ws` library) aborts with "Server sent no subprotocol".
  The web-shell now offers a non-secret marker (`qwen-ws`) alongside the
  bearer one, so the daemon always selects the marker — never echoing the
  secret — and the handshake completes for strict and lenient clients
  alike.
- Tests: add daemon-side coverage for the subprotocol auth path (accept
  valid token, reject wrong/malformed, no-token loopback) and assert the
  secret is never echoed back; add client coverage that the bearer token
  is offered as `[qwen-ws, qwen-bearer.<b64url>]` and round-trips.
- Drop the unreachable try/catch around `Buffer.from(_, 'base64url')`
  (it never throws) and correct the comment.
- a11y: mark the transcribing/interim regions as `role="status"` /
  `aria-live`, and add a `:focus-visible` ring to the idle mic.
- Refresh the stale `useVoiceCapture` doc comment (token deployments are
  now supported in the browser) and fix the mock typing in
  VoiceButton.test.tsx.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): ignore stale voice socket callbacks

* test(serve): sync voice capability integration expectation

* fix(serve): harden voice websocket review gaps

* fix(serve): sanitize voice websocket failures

* fix(serve): address voice review suggestions

* fix(serve): harden voice websocket lifecycle

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-06-24 11:32:43 +08:00
qqqysandQwen-Coder b9c5e3566b feat(voice): voice dictation with native capture, streaming, and biasing (#5502)
* feat(voice): voice dictation with native capture, streaming, and biasing

Add voice dictation for the prompt input:
- /voice [hold|tap|off|status] command + general.voice.{enabled,mode,language,protocol} settings; push-to-talk via Space, /model --voice to pick the model
- Native microphone capture (@qwen-code/audio-capture, miniaudio N-API) with arecord/SoX fallback, silence auto-stop, cold-start warm-up, and macOS permission query
- Batch transcription via DashScope Qwen-ASR (OpenAI-compatible chat/completions + input_audio) with language + keyterm biasing and an echo guard
- Live streaming over the DashScope realtime WebSocket (fun-asr-realtime / paraformer-realtime-v2) with interim text and an input-level waveform, behind voice.protocol=dashscope-realtime
- Rich VoiceIndicator UI (state, level meter, live partial transcript)
- Cross-platform prebuilds via prebuildify + node-gyp-build and a CI matrix

Co-authored-by: Qwen-Coder <[email protected]>

* feat(cli): route voice ASR by model

* feat(cli): polish voice realtime parity

* ci: fix voice workflow checks

* fix(cli): address voice review blockers

* fix(cli): harden voice transcription failures

* fix(cli): address voice PR review blockers

* fix(cli): harden voice review follow-ups

* fix(voice): handle realtime review blockers

* fix(cli): add voice command i18n keys

* fix(cli): address voice review blockers

* fix(cli): address voice review follow-ups

* fix(voice): harden review edge cases

* fix(voice): address release and realtime review blockers

* fix(voice): address realtime suggestion followups

* fix(voice): handle realtime review followups

* fix(voice): address realtime review blockers

* fix(voice): address review feedback

* fix(voice): address recorder review feedback

* docs(voice): document ssrf guard boundary

* fix(voice): address review feedback

* fix(voice): preserve warm recorder session safety

* fix(voice): address stream review suggestions

* fix(voice): address multi-round review findings

Realtime/streaming:
- Salvage an already-committed transcript when the WebSocket closes right
  after finish() instead of rejecting the whole dictation
  (qwenAsrRealtimeSession, voiceStreamSession) + regression tests.

useVoiceInput state machine:
- Single-shot finalize guard so a tap-stop racing the silence auto-stop can't
  double-stop the recorder and surface a spurious failure.
- Reset mountedRef on (re)mount so StrictMode (DEBUG) can't freeze the voice UI.
- Widen the hold-mode first-press release window above common key-repeat delays.

Model selection:
- Reject ids with no ASR transport at /model --voice and in the model dialog via
  a new isSelectableVoiceModel; move resolveVoiceTransport into voiceModel so the
  record-time config resolver stays transport-agnostic (+ tests).

macOS mic permission:
- Surface the not-determined state in voice warmup so the first dictation isn't
  silently lost behind the TCC dialog.

Native packaging:
- Make the audio-capture native install non-fatal (falls back to SoX/arecord) so
  a voice-only build failure can't break npm ci.
- Download audio-capture prebuilds before building standalone release archives.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(voice): address review blockers

* fix(voice): update batch recording audio level

* fix(voice): tap-mode transcript loss, stream leaks, and dead keyterm cleanup

- Tap-mode dictation submitted a stale empty buffer.text, wiping the just-
  inserted transcript and sending nothing: thread the resulting prompt text
  through onSubmit(text) instead of reading buffer.text back synchronously
  after the async insert (useVoiceInput, InputPrompt).
- Streaming finalize leaked the WebSocket session when recorder.drain() threw:
  abort the session before propagating the error.
- voiceStreamSession: reject the connect promise when 'task-finished' arrives
  before 'task-started' instead of hanging forever in 'transcribing'.
- Remove dead keyterm enrichment (project/branch/recent-file paths were
  unreachable after the privacy fix) and the now-inert CJK echo guard, plus the
  tests that asserted that removed behavior; fix the misleading "OpenAI prompt
  field" comment.
- Add the missing 'Voice Model' and macOS mic-permission i18n keys to
  en/zh/zh-TW (they were falling back to English; check-i18n doesn't flag keys
  absent from en).

Co-authored-by: Qwen-Coder <[email protected]>

* fix(voice): reject partial stream transcripts

* fix(cli): let dialogs consume voice keys first

* fix(voice): reject incomplete qwen realtime transcripts

* fix(voice): handle stream review blockers

* fix(voice): salvage qwen realtime transcript on close

* fix(voice): sanitize streamed transcript text

* test(audio): run audio capture tests in CI

* fix(voice): address review blockers

* fix(voice): report stream close while recording

* fix(voice): reduce keyterm echo false positives

* fix(voice): handle realtime close diagnostics

* fix(voice): address realtime review blockers

* fix(lint): allow legacy voice filenames

* chore(cli): rename voice files to kebab-case

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-06-22 14:33:36 +08:00
Goveandqqqys 0ba245ea3d feat(cli): add persistent history collapse on resume with refined commands (#4085)
* feat(cli): add --quiet-restore flag to suppress history output on session resume

* fix: preserve history state for /rewind while suppressing rendering

* refactor: model quiet-restore as display policy with shared utilities

* refactor: replace --quiet-restore with /history collapse|expand slash command

* fix: persist history collapse state as user setting

* fix(cli): address maintainer feedback on history collapse persistence and i18n

* test(cli): fix TypeScript compilation errors in historyCommand tests

* fix(cli): address maintainer review feedback on history collapse

* test: fix act() warning in slashCommandProcessor.test.ts

* fix: make applyCollapsePolicyAndSummary pure to avoid React batching bug

* chore: revert unrelated changes to lockfile and NOTICES.txt

* test: verify isRealUserTurn handles suppressOnRestore items correctly

* wip(cli): preserve local history review fixes before redesign

* feat(cli): refine history resume collapse commands

* fix(cli): address maintainer review feedback on history collapse

* test(cli): cover cold-boot collapsed resume

* fix(cli): address reviewer feedback on history collapse

* fix(cli): resolve rebase conflicts and missing imports

* fix(cli): strip suppressOnRestore in handleRewindConfirm

* fix(i18n): add Chinese translations for history collapse commands

* fix: address wenshao review comments on PR #4085

- Restore restoreGoalFromHistory call in cold-boot resume path
- Extract stripSuppressOnRestore to shared utility in resumeHistoryUtils
- Add comment explaining historyRef pattern in slashCommandProcessor
- Use historyCommand.name constant instead of string literal
- Add missing i18n translation for collapse summary message
- Fix pluralization in createHistoryCollapseSummaryItem

* fix(i18n): add missing English translations for history collapse commands

* fix: address wenshao follow-up review comments

- Filter out collapse-summary items in rewind path (AppContainer.tsx)
- Show info messages for collapse-on-resume/expand-on-resume commands (slashCommandProcessor.ts)
- Use visibleHistory instead of uiState.history in summaryByCallId useMemo (MainContent.tsx)
- Remove dead hasHistoryManager guard and optional chaining (useResumeCommand.ts)

* fix: restore optional chaining for remount in useResumeCommand

* test: update slashCommandProcessor tests for history command feedback changes

* chore: remove generated artifact files from branch

- Remove .learnings/LEARNINGS.md (local workflow artifact)
- Remove .pr-body.md (PR description draft)
- Remove build_output.log (build log)
- Remove vscode_test_output.log (test output log)

These files are unrelated to the history-collapse feature and were causing
git diff --check whitespace errors.

* fix: address wenshao review comments on PR #4085

- Remove stray [!tip] file from repo root
- Add selfManaged flag to MessageActionReturn for explicit UI feedback control
- Fix expand-now to return load_history type instead of calling loadHistory directly
- Replace hardcoded isSelfManaged path check with result.selfManaged in processor
- Fix MainContent merge detection to use visibleHistory.length (avoid flicker)
- Refactor applyCollapsePolicyAndSummary to not mutate input array
- Extract expandCollapsedHistory shared helper
- Restore dialog:memory test in slashCommandProcessor.test.ts
- Add stripSuppressOnRestore dedicated tests
- Add visibleHistory filtering test in MainContent.test.tsx
- Update historyCommand tests for new load_history return type
- Fix eslint errors: remove unused imports and fix dependency array

* fix: address wenshao review comments on PR #4085

- Remove stray [!tip] file from repo root
- Add selfManaged flag to MessageActionReturn for explicit UI feedback control
- Fix expand-now to return load_history type instead of calling loadHistory directly
- Replace hardcoded isSelfManaged path check with result.selfManaged in processor
- Fix MainContent merge detection to use visibleHistory.length (avoid flicker)
- Refactor applyCollapsePolicyAndSummary to not mutate input array
- Extract expandCollapsedHistory shared helper
- Restore dialog:memory test in slashCommandProcessor.test.ts
- Add stripSuppressOnRestore dedicated tests
- Add visibleHistory filtering test in MainContent.test.tsx
- Update historyCommand tests for new load_history return type
- Fix eslint errors: remove unused imports and fix dependency array
- Fix TypeScript errors: add useEffect import, fix display.kind type assertions

* fix: add braces around if statement body (eslint curly rule)

Fixes lint error in editorGroupUtils.ts:
- Expected { after 'if' condition on line 32

* fix: address wenshao follow-up review comments on PR #4085

- Revert expand-now to use loadHistory/refreshStatic directly (no load_history return)
- Remove dead selfManaged flag from MessageActionReturn and processor
- Fix visibleHistory filter to also exclude collapse-summary items
- Simplify applyCollapsePolicyAndSummary (return rawItems when !collapseOnResume)
- Fix test assertion shapes (content→text, timestamp as separate arg)
- Add mockClient for expand-now test
- Update historyCommand tests for new behavior
- Add expandCollapsedHistory dedicated tests
- Fix MainContent filtering test to use historyItemDisplayPropsSpy

* fix: address wenshao latest review comments on PR #4085

- Fix visibleHistory filter: remove collapse-summary exclusion (summary should render when all items suppressed)
- Update MainContent test: assert summary item IS rendered alongside unsuppressed items
- Remove dead selfManaged flag from MessageActionReturn type
- Remove dead selfManaged check from slashCommandProcessor.ts
- Add remount?.() to useResumeCommand error path
- Remove unused 'History expanded.' translation key from en.js, zh.js, zh-TW.js
- Fix expand-now test: mock action to return undefined (matching real behavior)

* fix: address wenshao latest review comments on PR #4085

- Add missing i18n translations to ca.js, de.js, fr.js, ja.js, pt.js, ru.js
- Simplify applyResumeDisplayPolicy: remove dead options parameter
- Fix expand-now test: mock action to return undefined (matches real behavior)
- Revert unrelated changes to package-lock.json and editorGroupUtils.ts

* fix: address wenshao latest review comments

- Add openDiffDialog to createMockActions() in slashCommandProcessor.test.ts
- Add sentToModel: false to user message assertions in slashCommandProcessor.test.ts
- Fix useResumeCommand.test.ts mock: spread original @qwen-code/qwen-code-core exports to include createDebugLogger
- Remove dead optional chaining (addItem?., clearItems?., loadHistory?.) in useResumeCommand.ts
- Simplify if (!config || !startNewSession) to if (!config) since startNewSession is required
- Fix truncatedCount off-by-one in AppContainer.tsx rewind path: exclude collapse-summary from effective length
- Apply collapse policy (applyCollapsePolicyAndSummary) in useBranchCommand.ts for /branch
- Add settings to UseBranchCommandOptions with proper useCallback dependency

* fix: add missing mockUpdateItem arg to test renderHook calls, remove stray file

- Add mockUpdateItem (17th arg) to resume-direct and memory dialog test calls
- Remove accidentally committed packages/core/.qwen/computer-use/installed.json
- Add .qwen/computer-use/installed.json to .gitignore

* fix: address review comments (type, gitignore, test, split-brain)

1. UIActionsContext: handleResume return type → Promise<void>
2. .gitignore: split corrupted merged line into .codegraph + .qwen/...
3. useBranchCommand.test: add settings to makeOptions(), add collapseOnResume test
4. useResumeCommand: reorder core-before-UI with rollback (matches branch pattern)
5. useResumeCommand.test: add getSessionId to mocks, add rollback test

* fix: complete history collapse translations in 6 locale files

- Added missing translation text for 'History collapsed' message
- Fixed syntax errors in de, fr, ja, pt, ru, zh-TW locale files

* fix: add missing history parameter in slashCommandProcessor test

- Fixed parameter order in 'should skip reload when consumeSlashReloadSuppression' test
- Added missing empty array for history parameter
- All 58 tests now pass

* merge: resolve conflicts with upstream main

- docs: keep ui.history.collapseOnResume setting, adopt upstream showCitations default
- fix: update rewindRecording call to include file history snapshots parameter

* fix(cli): repair history collapse CI failures

---------

Co-authored-by: qqqys <[email protected]>
2026-06-19 19:22:48 +00:00
pomelo 5715b3450d docs: rewrite CLAUDE.md to point to AGENTS.md as authoritative source (#5138) 2026-06-15 15:23:26 +08:00
callmeYeandClaude Opus 4.6 be31e8493c perf(filesearch): move AsyncFzf index construction to a worker thread (#4621)
* perf(filesearch): move AsyncFzf index construction to a worker thread

`new AsyncFzf(allFiles, ...)` is misleadingly named — its constructor is
synchronous and dominates the @-picker's main-thread cost on workspaces
with >20k files (where the v2→v1 algorithm switch kicks in). It's the
remaining freeze source after PR #3214 moved the crawl onto async spawn-
based git ls-files / ripgrep / fdir.

This change hosts the AsyncFzf instance in a worker_threads worker so the
construction happens off the main thread; only completed find() results
cross the message channel. Below ~5k files the in-thread path is kept
because worker spawn + IPC overhead exceeds the construction cost there.

Notes:
- `FzfWorkerHandle.create()` awaits the worker `ready` event before
  returning, so init errors surface at create() time rather than as a
  surprise on the first find().
- Worker is `unref()`-ed so a mid-find @-picker doesn't pin process exit.
- `FileSearch` interface gains an optional `dispose?(): Promise<void>`
  to release worker resources. `FileMessageHandler.clearFileSearchCache`
  calls it so the long-running VS Code extension host doesn't accumulate
  workers across watcher-triggered evictions.
- `installInProcessFzfTransport()` is provided for tests / sandboxed
  environments that need to skip worker spawn entirely.
- Bundle gains a second esbuild build that emits `dist/fzfWorker.js`
  alongside `dist/cli.js`; `prepare-package.js` ships it in the tarball.
- In `npm run dev` (tsx) the bundled worker file isn't on disk, so the
  handle silently falls back to in-thread — acceptable for a dev script.

Closes the door on #3455's two open `FileIndexService` review items by
design: there is no `whenReady()` (handle awaits ready inside `create()`)
and there is no in-worker crawl (crawl stays on main, only fzf moves).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(filesearch): address review feedback on fzf worker PR

- Fix worker thread leak in useAtCompletion: dispose previous FileSearch
  instance on re-init and component cleanup
- Fix worker leak on init failure: wrap inst.ready() in try/catch with
  timeout, dispose on failure
- Fix find()-after-dispose hang: reject immediately when worker is disposed
- Add limit field to FindMessage protocol to cap IPC payload before
  postMessage serialization
- Add real worker_threads test coverage (spawn/find/dispose lifecycle,
  concurrent finds, init failure, post-dispose rejection)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(filesearch): address second round of review feedback

- Fix useAtCompletion cleanup: move dispose() to [cwd,config] effect so
  it only fires on workspace change, not every keystroke/status transition
- Clear setTimeout in Promise.race on happy path to prevent timer leak
- Dispose FileSearch instances in FileMessageHandler on panel close
- Fix incorrect comment in prepare-package.js (resolveBundleDir, not URL)
- Add worker crash test: verify failAll rejects pending find() on exit
- Add RecursiveFileSearch.dispose() test in fileSearch.test.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(core): close missing braces in formatDateForContext test

The second test case in the formatDateForContext describe block was
missing its closing `});`, causing a TS1005 parse error that broke CI
on all platforms. This was introduced upstream in 6a99ed150.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(filesearch): guard initialize() against races and stale completions

- Add cancelled flag to initialize() effect so a superseded or
  unmounted init disposes the newly-created searcher instead of
  writing it into fileSearch.current with no owner
- Add fzfWorker.js test artifact to .gitignore to prevent accidental
  commits if the test process crashes before afterAll cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(filesearch): mark worker handle as failed on post-init crash

failAll() previously only set readyState to 'failed' when the worker
crashed during init (state 'pending'). A crash after successful init
left the handle in 'ready' state, causing subsequent find() calls to
silently return empty results via the catch handler. Now failAll()
unconditionally marks the handle as 'failed' (unless already disposed),
and find() rejects immediately on a failed handle.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-12 11:47:16 +08:00
顾盼 27b056b777 feat(skills): enforce auto-skill- directory prefix for auto-generated skills (#4839)
* feat(skills): enforce auto-skill- directory prefix for auto-generated skills

Auto-generated skills created by the managed-skill-extractor agent now use a
mandatory `auto-skill-` directory prefix, and `.gitignore` re-ignores
`.qwen/skills/auto-skill-*/` so these transient, session-specific directories
no longer surface as untracked files in `git status`.

- .gitignore: add `.qwen/skills/auto-skill-*/` after the `!.qwen/skills/**`
  unignore rule. Git's last-rule-wins semantics re-ignore only the prefixed
  directories while hand-authored project skills stay tracked. Existing
  auto-generated skills are not renamed.
- skillReviewAgentPlanner.ts: add an AUTO_SKILL_DIR_PREFIX constant and require
  the prefix in SKILL_REVIEW_SYSTEM_PROMPT and buildTaskPrompt(). The frontmatter
  `name:` stays the natural name so skills keep their normal invocation. This is
  a prompt-level convention only — SkillManager discovery is prefix-agnostic and
  `source: auto-skill` remains the file-level edit-protection marker.
- add a test asserting buildTaskPrompt() emits the prefix instruction.

Closes #4837

* test(skills): assert system-prompt prefix + reserve auto-skill- namespace

Follow-up to reviewer verification on #4839.

- Export SKILL_REVIEW_SYSTEM_PROMPT and add a unit test asserting it carries
  the `auto-skill-` prefix instruction. The prefix mandate lives on two
  independent string arrays (SKILL_REVIEW_SYSTEM_PROMPT and buildTaskPrompt);
  only the latter was guarded, so an edit could silently drop the mandate from
  the system-prompt layer. The new test closes that gap.
- Extend the .gitignore comment to reserve `auto-skill-` as an auto-generated
  namespace, noting that a hand-authored skill using this prefix would be
  ignored — preventing a silent-untrack surprise.
2026-06-08 17:07:00 +08:00
Dragon bec97f445d feat(skills): add agent reproduction workflows (#4118)
* chore(skills): add codex reproduce workflows

* feat(agent-reproduce): implement agent reproduction workflow and supporting scripts

* feat(skills): capture reference agent state diffs
2026-06-02 12:10:54 +08:00
4dc98484fd feat(cli): do not append trailing space for directory completions (#4092) (#4288)
* feat(cli): do not append trailing space for directory completions (#4092)

## What

在 @路径补全和 /dir add 命令的目录补全中不再追加尾部空格。这样可以允许用户在补全目录后直接按 Tab 继续深入下一级子目录,无需先删除空格。

## Examples

- Input: `@src/com` + Tab → Output: `@src/components/` (no trailing space)

- Input: `/dir add ./pac` + Tab → Output: `/dir add ./packages/` (no trailing space)

- File completions still append a space (e.g., `@src/file.txt `)

## Changes

- Added `isDirectory` flag to `Suggestion` and `CommandCompletionItem` interfaces

- Updated `handleAutocomplete` to skip trailing space when `isDirectory === true`

- Modified `getDirPathCompletions` to return `CommandCompletionItem[]` with `isDirectory: true`

- Added test case for directory completion behavior

* fix(cli): append trailing / to directory completions for deeper navigation

* fix(cli): propagate isDirectory and fix JSDoc comment

## Comment 2: Fix JSDoc in SuggestionsDisplay

Removed "(ends with /)" from isDirectory description since it was factually incorrect.

## Comment 3: Add test for isDirectory propagation

- Added test suite in useSlashCompletion.test.ts to verify directory command structure

- Real filesystem testing is done in directoryCommand.test.tsx

* fix(cli): add comprehensive isDirectory propagation tests

Added getDirPathCompletions unit tests that verify:
- Directory suggestions include isDirectory: true
- Directory values end with / for continued navigation
- Prefix filtering preserves isDirectory flag
- Comma-separated path completion works correctly
- Deeply nested directories maintain isDirectory flag

This closes the testing gap identified in review comment 3.

* fix(cli): address wenshao feedback - lint rules, real test, cross-platform

Fixes 4 new review comments from wenshao:

- [Critical] Empty catch {} blocks: guarded with if (tempTestDir) + void err

- [Critical] useSlashCompletion.no-op test: replaced with real integration test that

  verifies isDirectory propagation through toSuggestion pass-through

- [Suggestion] Windows path separator: using path.sep instead of hardcoded /

  in both directoryCommand.tsx and related test assertions

* fix(cli): remove unused import and fix Windows path separator in tests

- Remove unused directoryCommand import in useSlashCompletion.test.ts (TS6133)
- Replace hardcoded / regex with path.sep-aware assertions in
  directoryCommand.test.tsx to fix Windows CI failures

Co-authored-by: Qwen-Coder <[email protected]>

* Apply suggestion from @wenshao

Co-authored-by: Shaojin Wen <[email protected]>

* Update packages/cli/src/ui/commands/directoryCommand.test.tsx

Co-authored-by: Shaojin Wen <[email protected]>

* Update packages/cli/src/ui/commands/directoryCommand.tsx

Co-authored-by: Shaojin Wen <[email protected]>

* Update packages/cli/src/ui/commands/directoryCommand.tsx

Co-authored-by: Shaojin Wen <[email protected]>

* fix(cli): normalize isDirectory to explicit boolean in toSuggestion

Normalize isDirectory from three-state (true/false/undefined) to explicit
boolean (true/false) to prevent latent bugs in future code that might
distinguish between false and undefined.

Fixes review comment: isDirectory normalization is inconsistent across
completion paths.

Co-authored-by: Qwen-Coder <[email protected]>

* Update packages/cli/src/ui/hooks/useSlashCompletion.ts

Co-authored-by: Shaojin Wen <[email protected]>

* chore: remove accidentally committed pr_body.md

Co-authored-by: Qwen-Coder <[email protected]>

* chore: add pr_body.md to .gitignore

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): remove duplicate .slice and orphaned test code from directoryCommand.tsx

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): only suppress trailing space for dir completions at end-of-line

When isDirectory is true, the trailing space was suppressed unconditionally,
even when the cursor is mid-line. This caused directory completions to merge
directly with following text (e.g. '@src/components/something').

Now only suppress the space when the cursor is at end-of-line, allowing
continued Tab navigation into subdirectories.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(cli): document crawler path separator dependency for isDirectory check

The isDirectory detection uses p.endsWith('/') which depends on the
crawler in @qwen-code/qwen-code-core normalizing paths with posix '/'
(fdir.withPathSeparator('/') in crawler.ts). Add a comment to make this
implicit coupling explicit.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): add mid-line directory completion test

Verify that directory completions append a trailing space when the cursor
is mid-line, preventing the completed path from merging with following text.

Co-authored-by: Qwen-Coder <[email protected]>

* Update packages/cli/src/ui/hooks/useCommandCompletion.test.ts

Co-authored-by: Shaojin Wen <[email protected]>

---------

Co-authored-by: 方磊 <[email protected].1.11>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-05-23 23:37:23 +08:00
28a3439307 feat(skills): Add codegraph skill for PR review risk analysis and conflict detection (#3910)
* feat(skills): add codegraph skill for PR review risk analysis and conflict detection

Add .qwen/skills/codegraph/ with PR analysis, bug analysis, schema,
patterns, and eval support. Enables per-PR risk scoring (blast radius,
interface changes, test coverage), cross-PR conflict detection, and
automated GitHub labeling via the codegraph-ai pip package.

* chore: ignore .venv and .codegraph directories

Co-authored-by: Qwen-Coder <[email protected]>

* docs(skills): add HF mirror and ModelScope fallback for embedding model downloads

Co-authored-by: Qwen-Coder <[email protected]>

* fix(skills): address PR #3910 review feedback for codegraph skill

- Rename skill from codegraph-qa to codegraph for consistency
- Broaden description to cover index creation and inspection
- Add HuggingFace/ModelScope model download tips
- Document codegraph ingest command for adding git history
- Fix stray '@' in stdout Tee pattern code snippet
- Remove dead reference to PRReview.md
- Add repo_dir parameter to CrossPRAnalyzer examples
- Update CHANGES edge types (hunk/deleted/related/new) and
  resolve_pr_functions internals
- Add Conflict Detection Dimensions table
- Replace codegraph query with PRReview Python API examples
- Remove OPTIONAL MATCH limitation from schema (now supported)
- Update codegraph-ai link to PyPI
- Add HF model download entry to Troubleshooting table

---------

Co-authored-by: pomelo-nwu <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-05-11 18:14:40 +08:00
易良 cb7059f54d feat(installer): add standalone archive installation (#3776)
* feat(installer): add standalone archive installation

* fix(installer): harden standalone archive installs

* fix(installer): address standalone review findings

* chore(installer): clarify review followups

* fix(installer): stabilize standalone script checks

* chore(installer): remove internal planning docs

* chore(installer): simplify standalone release review fixes

* test(installer): add Windows batch install smoke

* test(installer): fix Windows batch smoke quoting

* test(installer): preserve Windows cmd quotes

* fix(installer): use robust Windows checksum hashing

* ci: narrow installer debug matrix

* fix(installer): address standalone review hardening

* fix(installer): avoid Windows validation parse errors

* fix(installer): simplify Windows option validation

* fix(installer): harden standalone review fixes
2026-05-11 13:25:48 +08:00
pomeloandQwen-Coder 7fe853a782 Feat/openrouter auth (#3576)
* feat(cli): add OpenRouter auth flow

Co-authored-by: Qwen-Coder <[email protected]>

* feat(cli): add OpenRouter model management UI

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): align OpenRouter OAuth fallback session

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(cli): unify OpenRouter model setup flow

Co-authored-by: Qwen-Coder <[email protected]>

* feat(auth): update OAuth description with provider examples and i18n support

- Updated OAuth option description to include provider examples (OpenRouter, ModelScope)
- Added internationalization support for new description text
- Updated all language files (en, zh, de, fr, ja, pt, ru) with translations

Co-authored-by: Qwen-Coder <[email protected]>

* docs: simplify OpenRouter design docs

Co-authored-by: Qwen-Coder <[email protected]>

* test(auth): fix OpenRouter OAuth mock typing

Co-authored-by: Qwen-Coder <[email protected]>

* test(auth): sync AuthDialog tests with new three-option main menu layout

Co-authored-by: Qwen-Coder <[email protected]>

Update assertions that referenced removed 'Qwen OAuth' and 'OpenRouter' options in the main/API-key views to match the refactored OAUTH / CODING_PLAN / API_KEY structure.

* fix(i18n): add missing zh-TW translation for browser-based auth key

Co-authored-by: Qwen-Coder <[email protected]>

zh-TW.js was generated from main's en.js which had already removed this key, but the PR re-adds it in en.js. Sync zh-TW with the new translation.

* feat(cli): Improve custom auth wizard with step indicators and cleaner advanced config (#3607)

* feat(cli): Add custom API key auth wizard with 6-step setup flow

Replace the documentation-only

Co-authored-by: Qwen-Coder <[email protected]>"Custom API Key" screen with an
in-terminal wizard: Protocol select → Base URL input → API Key input →
Model ID input → JSON review → Save.

- Add 5 new ViewLevels and render functions in AuthDialog
- Implement utility functions: generateCustomApiKeyEnvKey (normalization),
  normalizeCustomModelIds (split/trim/dedupe), maskApiKey (display)
- Implement handleCustomApiKeySubmit in useAuth with backup, env key
  generation, modelProviders merge, auth refresh, and user feedback
- Wire handler through UIActionsContext and AppContainer
- Add 18 unit tests for utilities, 4 wizard flow integration tests

* feat(cli): Improve custom auth wizard with step indicators and cleaner advanced config

- Add step indicators (Step 1/6 · Protocol) to each wizard screen
- Remove redundant Protocol/Endpoint context from each step for focus
- Redesign advanced config: add descriptions to thinking/modality toggles
- Remove max tokens option; keep only thinking and modality settings
- Add ↑↓ arrow navigation with Space toggle and Enter to continue
- Generation config flows through review JSON and final submit

Co-authored-by: Qwen-Coder <[email protected]>

* test: Fix Windows CI failures in fileUtils and AuthDialog tests

- fileUtils.test.ts: Mock node:child_process execFile to prevent
  pdftotext spawn that times out on Windows (ENOENT, 5s timeout)
- AuthDialog.test.tsx: Add char-by-char typeText() helper to work
  around Node 24.x + ink TextInput compatibility issue on Windows

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): Reset advanced wizard state and use JSON.stringify for settings preview

- Reset advancedThinkingEnabled, advancedModalityEnabled, and
  focusedConfigIndex when re-entering custom wizard to prevent
  state leakage between configurations
- Replace hand-rolled JSON string concatenation with
  JSON.stringify for settings.json preview to properly escape
  special characters in model IDs and base URLs

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): harden OpenRouter OAuth callback handling

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): stabilize OpenRouter state mismatch test

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): stabilize custom auth wizard navigation

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-04-27 14:47:44 +08:00
jinyeandjinye.djy 96bc874197 chore(gitignore): add .codex directory to gitignore (#3665)
🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

Co-authored-by: jinye.djy <[email protected]>
2026-04-27 14:38:56 +08:00
e47b22806b feat(docs): add qwen-code skills, agents, and updated AGENTS.md (#3575)
- Add new skills: bugfix, feat-dev with structured workflows
- Update existing skills: docs-audit-and-refresh, docs-update-from-diff,
  e2e-testing, qwen-code-claw, structured-debugging, terminal-capture
- Update test-engineer agent with clearer constraints and formatting
- Update qc commands: bugfix, code-review, commit, create-issue, create-pr
- Reorganize .gitignore to keep qwen configs near top
- Expand AGENTS.md with development commands, feature/bugfix workflows,
  project directories table, and code review guidelines

Co-authored-by: 愚远 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-04-24 17:33:03 +08:00
tanzhenxinandQwen-Coder dc833d9d94 feat: add bugfix workflow, test-engineer agent, and debugging skills
- Add test-engineer agent for bug reproduction and verification
- Add /qc:bugfix command for structured bugfix workflow
- Add e2e-testing skill covering headless/interactive modes, MCP testing
- Add structured-debugging skill for hypothesis-driven debugging
- Simplify AGENTS.md to focus on essential commands and conventions
- Add terminal-capture scenario for bugfix workflow testing
- Add .qwen folder to ESLint ignore list

Known limitations: The /qc:bugfix workflow and e2e-testing skill
are experimental and may be unstable or consume significant tokens.

Co-authored-by: Qwen-Coder <[email protected]>
2026-04-04 18:30:09 +08:00
LaZzyMan 7a6b725b0c feat: replace qwen-settings-config with bundled qc-helper skill
- Remove project-level qwen-settings-config skill and its references/
- Create bundled qc-helper skill at packages/core/src/skills/bundled/
  that references docs/users/ for answering usage/config questions
- Update copy_bundle_assets.js to copy docs/users/ into dist/bundled/qc-helper/docs/
- Update dev.js to create symlink for dev mode docs access
- Add bundled docs directory verification in prepare-package.js
- Revert doc-update skills (docs-audit-and-refresh, docs-update-from-diff)
  to main branch versions
2026-03-27 12:03:00 +08:00
DragonnZhang a8bc25beb9 feat(skills): add docs audit and update helpers 2026-03-15 20:40:31 +08:00
tanzhenxin bca799e818 Merge branch 'main' into feat/ask-user-question-tool 2026-03-06 14:03:42 +08:00
tanzhenxinandQwen-Coder a172696b86 Merge branch 'main' into feat/support-insight-command
Co-authored-by: Qwen-Coder <[email protected]>
2026-02-26 21:05:41 +08:00
tanzhenxinandQwen-Coder 9e4c5ee891 refactor: Extract web-templates package and unify build/pack workflow
Moves export-html and insight templates from cli/assets to a new
dedicated web-templates package. Updates Dockerfile and build scripts
to use consolidated bundle/prepare:package/pack workflow.

Co-authored-by: Qwen-Coder <[email protected]>
2026-02-26 21:02:46 +08:00
pomelo bec8cbc575 Merge branch 'main' into feat/terminal-capture 2026-02-25 15:26:57 +08:00
DragonnZhangandQwen-Coder ad23df4021 chore: exclude .qwen/commands/ and .qwen/skills/ from gitignore
- Add negation rules to preserve Qwen commands and skills directories
- This allows version control for custom commands and skills in .qwen/

Co-authored-by: Qwen-Coder <[email protected]>
2026-02-15 22:33:14 +08:00
pomelo-nwuandQwen-Coder 997fcbfaed feat: add terminal-capture for CLI screenshot automation
- Add terminal-capture engine using node-pty + xterm.js + Playwright
- Add scenario runner with TypeScript configuration
- Add pre-built scenarios (/about, /context, /export, /auth)
- Add Cursor skills for terminal-capture and pr-review workflow
- Add motivation documentation

Co-authored-by: Qwen-Coder <[email protected]>
2026-02-14 21:34:42 +08:00
DragonnZhang 12b669d7c6 feat: update .gitignore to include .claude and CLAUDE.md; add project context documentation in QWEN.md 2026-02-13 15:47:58 +08:00
yiliang114andQwen-Coder 02db22dd78 chore: update eslint config and translation adjustments
Co-authored-by: Qwen-Coder <[email protected]>

This commit includes several updates:

- Updated eslint configuration with improved formatting and organization

- Added SaveMemoryToolCall support in vscode-ide-companion

- Translated Chinese comments and UI text to English in various components

- Made minor code formatting improvements across multiple files
2026-01-29 19:44:37 +08:00
yiliang114 878a870df2 Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/unified-ui-for-vscode-extension 2026-01-29 15:40:32 +08:00
tanzhenxin e1326d9e72 Merge pull request #1640 from QwenLM/feat/concurrent-runner
feat: add concurrent runner for batch CLI execution
2026-01-29 14:22:31 +08:00
pomelo-nwu 021680cd70 feat: add html template 2026-01-28 19:56:20 +08:00
tanzhenxinandQwen-Coder f48eec9a02 feat: add git diff capture and session log collection to concurrent runner
- Add git diff capture after each run, saved as diff.patch
- Add session log collection from ~/.qwen/projects/{id}/chats/
- Store session logs in outputs/{run_id}/chats/ with original filename
- Add session_id field to track chat recording UUID
- Modify cwd in session logs to actual runner working directory
- Remove stdout_file/stderr_file from top-level, keep only in prompt_results
- Rename logs folder to openai-logs
- Add File Writer task example for testing file creation

Co-authored-by: Qwen-Coder <[email protected]>
2026-01-28 14:57:15 +08:00
tanzhenxin b9a0d904de feat: add multi-modal input support (image, PDF, audio) across all content generators 2026-01-21 15:44:58 +08:00