mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
80 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
23c6c7032a
|
feat(mobile-mcp): vendor mobile-mcp with opt-in 0-1000 relative coordinates (#6235)
* Squashed 'packages/mobile-mcp/' content from commit c5d7d27fd git-subtree-dir: packages/mobile-mcp git-subtree-split: c5d7d27fd61e4762e15ae4b1c68b6c011be88bb7 * feat(mobile-mcp): vendor mobile-mcp with opt-in 0-1000 relative coordinates Fork mobile-next/mobile-mcp (v0.0.61) into packages/mobile-mcp/ via git subtree, renamed to @qwen-code/mobile-mcp with the following additions: Relative coordinate shim (src/coord-norm.ts): - MOBILE_MCP_COORDINATE_SPACE=1 enables 0-1000 normalized coordinates - MOBILE_MCP_COORDINATE_SCALE configurable (default 1000, 999 for mobile_use) - Input denormalization for click/double_tap/long_press/swipe - Output normalization for list_elements and get_screen_size - Tool description rewriting when enabled - Default off = zero behavior change Android enhancements: - mobile_install_app: -r/-g/-d/-t install flags (Android only) - mobile_ui_dump: full UIAutomator XML hierarchy dump - mobile_adb_pull / mobile_adb_push: file transfer via ADB Infrastructure: - cd-mobile-mcp.yml: npm publish workflow (tag mobile-mcp-v*) - scripts/sync-from-upstream.sh: git subtree pull for upstream sync - .vendored-from / .vendored-patches.md: vendoring metadata - Upstream telemetry disabled by default - eslint.config.js: exclude packages/mobile-mcp from root lint * chore(mobile-mcp): update package-lock.json for workspace dependencies * fix(mobile-mcp): quote all YAML strings to pass yamllint * fix(mobile-mcp): fix cd workflow yaml to pass both yamllint and actionlint * fix(mobile-mcp): address review findings on our additions - ensureScreenSize: log warning instead of silent failure (#4) - invalidateScreenSize on orientation change (#5) - adb_push: path.posix.resolve to prevent /sdcard/ traversal (#6) - adb_pull: readOnlyHint → destructiveHint (writes local file) (#9) - adb_push: remove validateOutputPath on read-source local_path (#11) - normalizeElementResult: log error instead of bare catch (#16) - rewriteDescription: remove dead duplicate regex (#17) - cd workflow: add test step between build and publish (#19) * fix(mobile-mcp): update server.json identity and fix package.json main entrypoint --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
32d2f4674d
|
ci: add fork PR safety precheck (#5926)
* ci: add fork PR safety precheck * fix(ci): lint PR safety precheck script |
||
|
|
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 chrome-devtools-mcp@1.4.0 + puppeteer-core@25.2.0: - 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 <noreply@anthropic.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> |
||
|
|
adc2bce414
|
feat(cua-driver): vendor qwen-cua-driver with opt-in 0–1000 relative coordinates (#5896)
* feat(cua-driver): vendor trycua/cua driver with 1000-normalized coordinate support
Vendor libs/cua-driver from trycua/cua into packages/cua-driver as the
basis for qwen-code's computer-use backend, adding an opt-in relative
(1000x1000 normalized) coordinate mode for Qwen-VL clients.
- coord_norm.rs: 0-1000 <-> pixel conversion, per-(pid,window_id) size
cache, tools/list description rewrite (TDD, 27 tests)
- ToolRegistry: normalized field + invoke input/output hooks
- protocol.rs: system-instruction coordinate wording switched by mode
- serve.rs: daemon list path description rewrite (input_schema aware)
- main.rs: CUA_DRIVER_RS_COORDINATE_SPACE env seed
Default coordinate_space=pixels => zero behavior change for existing
pixel clients. Set CUA_DRIVER_RS_COORDINATE_SPACE=normalized_1000 to
enable. Excludes rust/target build output.
* feat(cua-driver): make normalized coordinate scale configurable
Add CUA_DRIVER_RS_COORDINATE_SCALE (default 1000) so the normalization
full-scale can absorb the Qwen 999-vs-1000 cookbook ambiguity without a
recompile. norm_to_px/px_to_norm now take an explicit scale; denormalize_args
reads the process-wide COORDINATE_SCALE seeded once at startup from env.
* ci(cua-driver): add cross-platform release workflow for vendored driver
Standalone GitHub Action that builds, signs, and releases the vendored
cua-driver under packages/cua-driver. Adapted from upstream trycua/cua
cd-rust-cua-driver.yml:
- macOS: universal binary (lipo arm64+x86_64), codesigned + notarized into
CuaDriver.app using qwen-code's existing secrets (MAC_CSC_LINK cert +
App Store Connect API key notarization); Developer ID identity is
auto-discovered from the imported cert.
- Linux: x86_64 + arm64, built in debian:11 for a glibc 2.31 floor.
- Windows: x86_64 + arm64, unsigned (no EV cert, matches upstream).
- Release: softprops/action-gh-release on cua-driver-rs-v* tags or manual
dispatch, prerelease.
Triggered by tag push (cua-driver-rs-v*) or workflow_dispatch.
* chore(cua-driver): rebrand vendored driver as qwen-cua-driver
Rename the vendored trycua/cua driver so the fork installs and runs
independently of any upstream trycua install:
- binary cua-driver -> qwen-cua-driver
- bundle CuaDriver.app -> QwenCuaDriver.app
- bundle id com.trycua.driver -> com.qwencode.cua-driver
Updates the cargo/uia manifests, Info.plist, bundle/proxy launch paths,
permission/health-report wording, the install/build scripts, and the
cross-platform release workflow.
* feat(cua-driver): finish relative-coordinate mode — toggle, scale, zoom/move_cursor
- CUA_DRIVER_RS_COORDINATE_SPACE is now a 1/0 toggle (via is_env_truthy);
default off keeps pixel mode byte-identical to upstream.
- Thread CUA_DRIVER_RS_COORDINATE_SCALE through every coordinate surface
(was hardcoded 1000): input denormalization already used it; now the
rewritten screenshot dims, the tool/param descriptions, and the agent
instructions track the configured scale too.
- Normalize zoom (window basis) and move_cursor (screen basis) inputs and
rewrite their descriptions, alongside click/double_click/right_click/drag.
- Fix zoom on downscaled (Retina) windows: apply the get_window_state resize
ratio so the crop lands on the region the agent saw. Normalized mode only;
pixel-mode zoom unchanged.
All coordinate behavior stays gated on the normalized flag, so the default
(pixels) path is unchanged from upstream.
* chore(cua-driver): add upstream-sync script (git subtree unusable here)
`git subtree split --prefix=libs/cua-driver` hangs on a commit deep in
trycua/cua's history, so the subtree add/pull workflow isn't usable for
the vendored driver (and a pull would re-split + re-hang every time).
Add scripts/sync-from-upstream.sh instead: it git-diffs two upstream refs
(never walks the full history, so it dodges the hang), reprefixes the
libs/cua-driver delta to packages/cua-driver, and `git apply --reject`s it
on top of our local changes — conflicts land as *.rej for manual fixup.
Record the vendored version in .vendored-from and document the migration +
sync method in the design doc.
* chore(cua-driver): exclude vendored driver from qwen-code ESLint
The vendored packages/cua-driver tree carries upstream JS (e.g. the
test-harness Electron app) that doesn't follow qwen-code's lint rules and
fails CI. It is not a workspace package (no package.json) and is not
qwen-code TypeScript, so add it to eslint.config.js global ignores —
alongside packages/desktop/** — the standard treatment for vendored code.
* fix(cua-driver): let start_session revive an idle-reaped session
Ports the fix from upstream trycua/cua#2035 into the vendored driver.
When a session is reaped for idleness, a subsequent start_session with the
same id failed instead of resuming it. Revive the ended session in place so
the agent can continue rather than getting a hard error.
* fix(cua-driver): retry daemon socket writes on EAGAIN
Ports the fix from upstream trycua/cua#2036 into the vendored driver.
A non-blocking daemon socket can return EAGAIN/EWOULDBLOCK mid-write when the
peer's receive buffer is momentarily full. The driver treated that as fatal
and dropped the connection. Add a bounded retry/poll loop (mirror of the
read-side socket_io helper) so transient back-pressure no longer kills the
session; only a real timeout or hard error fails the write.
* fix(cua-driver/linux): stop reporting bare "Clicked" for X11 synthetic clicks
Ports the fix from upstream trycua/cua#2025 into the vendored driver.
On X11, clicks are delivered via XSendEvent synthetic events, which many
toolkits (GTK/SDL/Allegro) ignore because send_event is set. The driver still
reported a flat success ("Clicked"), masking that nothing happened. Report
the synthetic-delivery caveat honestly so the agent can fall back instead of
assuming the click landed.
(platform-linux crate is not built on macOS; verified by clean upstream apply
and covered by upstream + release-workflow Linux CI.)
* fix(cua-driver/windows): list empty-/null-title top-level windows
Ports the fix from upstream trycua/cua#2021 into the vendored driver.
list_windows filtered out any top-level window whose title was empty or null,
so legitimate targets (splash screens, some Electron/game windows, tool
windows) were invisible to the agent and unclickable. Include empty-title
windows, using class name / process as a fallback label.
(platform-windows crate is not built on macOS; verified by clean upstream
apply and covered by upstream + release-workflow Windows CI.)
* chore(cua-driver): track cherry-picked upstream PRs; fix vendored-from
The vendored copy is actually at cua-driver-rs-v0.6.7 (workspace version and
all 0.6.7->0.6.8 delta files confirm it), but .vendored-from had drifted to
0.6.8 during an earlier sync-script trial whose code delta was not kept. Left
as-is it would make a future sync diff 0.6.8->newer and silently skip the real
0.6.7->0.6.8 fixes. Correct it back to 0.6.7.
Also record the four not-yet-merged upstream PRs we carry as cherry-picks
(trycua/cua#2021/#2025/#2035/#2036) in .vendored-patches.md, and have
sync-from-upstream.sh point at it so the next sync reconciles them.
* ci(cua-driver): satisfy repo yamllint on the release workflow
The vendored-driver release workflow tripped 114 quoted-strings violations
under the repo's .yamllint (quote-type: single, required). Single-quote all
string scalars to match every other workflow in .github/workflows.
While reformatting, the release-notes body also got its paragraph blank lines
collapsed and still referenced the old CUA_DRIVER_RS_COORDINATE_SPACE=
normalized_1000 value — restore the blank lines and update it to the current
0/1 toggle (default 0 = off; optional CUA_DRIVER_RS_COORDINATE_SCALE=1000).
* chore(cua-driver): sync vendored driver to cua-driver-rs-v0.6.8
First real run of scripts/sync-from-upstream.sh: it 3-way-applied the upstream
0.6.7->0.6.8 delta onto our local fork. 10/12 files applied cleanly; the 2
rejects (install.ps1, _install-rust.sh) were already-applied baked-version
bumps (0.6.6->0.6.7, our copies were already at 0.6.7), i.e. no real conflict.
0.6.8 brings: Wayland input path (platform-linux), linux health_report +
overlay tweaks, a platform-macos build.rs step, and dependency bumps. Version
moved to 0.6.8 across the workspace.
Verified our work survived the sync untouched: the relative-coordinate shim
(coord_norm/protocol) and all four cherry-picked PRs (socket_io/session +
linux/windows) are intact — in particular the 0.6.8 edit to platform-linux
tools/impl_.rs landed alongside our #2025 change with no collision. macOS
cargo check + 132 core tests green. (platform-linux/windows + the binary
integration test build only on their own runners; upstream CI covers those.)
* ci(cua-driver): add a dry_run gate to the release workflow
Mirror the desktop-release / release dry-run pattern: a workflow_dispatch
dry_run boolean input (default true). The cross-platform build + package jobs
always run and upload their artifacts; the GitHub Release job now publishes
only on a tag push or an explicit dry_run=false dispatch.
Lets us rehearse the whole build/package pipeline (dry_run=true, notarize=false)
and inspect the produced artifacts without cutting a release. A branch push
(no tag, not a dispatch) likewise builds without releasing.
|
||
|
|
3d6b6f7475
|
feat(lint): enforce kebab-case filenames with ESLint (#4797)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
Qwen Code CI / Integration Tests (CLI, No Sandbox) (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Co-authored-by: 俊良 <zzj542558@alibaba-inc.com> |
||
|
|
18cc73ce05
|
feat(web-shell): add extension management (#5398)
Co-authored-by: ytahdn <ytahdn@gmail.com> |
||
|
|
62e45c567f
|
feat(desktop): Add desktop app package with Qwen ACP SDK integration (#3778) | ||
|
|
174e8de179
|
fix(core): stop AbortSignal listener leak in long sessions (MaxListenersExceededWarning) (#4366)
* fix(core): consolidate AbortController handling to stop listener leaks in long sessions
Users hit `MaxListenersExceededWarning: 1509 abort listeners added to
[AbortSignal]` in long interactive sessions. The agent runtime nests
parent→child controllers (masterAbortController → per-message round →
per-API-call round → tool execution) and each layer registered its own
`addEventListener('abort', ...)` on the parent without `{once:true}` or
reverse cleanup, so listeners accumulated on long-lived parents across
hundreds of model turns.
Add `utils/abortController.ts` with three helpers:
- `createAbortController(maxListeners = 50)` — factory that pre-caps the
signal so the warning never fires on per-request signals.
- `createChildAbortController(parent)` — WeakRef-based parent→child
propagation with `{once:true}` on the parent listener AND a reverse-cleanup
listener on the child that detaches the parent listener when the child
aborts. This is the key mechanism — short-lived children stop accumulating
dead listeners on long-lived parents.
- `combineAbortSignals(signals, {timeoutMs})` — N-way combiner that replaces
the existing one-input `combinedAbortSignal.ts` (kept as a `@deprecated`
shim so `httpHookRunner.ts` doesn't churn).
Migrate every production `new AbortController()` in `packages/core/src` (24
sites) to the helper. Wrap `_runReasoningLoopInner` per-iteration body and
`AgentHeadless.execute` in `try/finally` so the round controller is aborted
(triggering reverse cleanup) even when the model stream or tool execution
throws. Add `{once:true}` to the manual abort listeners in `hookRunner`,
`functionHookRunner`, and `message-bus` that were missing it. Remove the
`raiseAbortListenerCap` band-aid from `openaiContentGenerator/pipeline.ts` —
no longer needed now that the per-round signal carries `maxListeners=50`.
Add `cli/utils/warningHandler.ts` as a belt-and-suspenders: hides
`MaxListenersExceededWarning.*AbortSignal` from end users in production
(any shape Node ≥20 emits), keeps it visible under `DEBUG`/`QWEN_DEBUG`/
`NODE_ENV=development`. Uses `process.on('warning', ...)` without
`removeAllListeners` so third-party warning subscribers stay intact.
Direct reproducer in `docs/verification/abort-controller-refactor/` proves
the old pattern accumulates 2000 listeners over 2000 rounds while the new
pattern stays at 0.
* fix(core): address PR #4366 review feedback
Four issues from the Copilot review:
1. combineAbortSignals — add a per-iteration `aborted` check inside the
for-loop so we short-circuit if an input signal flips aborted between
the initial scan and listener registration. In single-threaded JS this
can't actually interleave, but the defensive check makes correctness
obvious and protects against signals whose `aborted` getter has side
effects. New test exercises the path via a Proxy that flips after the
initial scan.
2. warningHandler docstring — was stale: said "AbortSignal / EventTarget"
while the regex was tightened to AbortSignal-only in the previous review.
3. README.md — replace personal absolute path with `$WT` placeholder so
the verification recipe is shareable.
4. README.md — replace the markdown table with per-scenario headed
sections. Prettier had interpreted an inline `ps -ef | grep sleep`
pipe character as a column separator, breaking the table rendering on
GitHub. Per-section format is also easier to scan and edit.
* test(core): fix abortController race-defense test to actually hit the loop check
The previous version set the Proxy's `aborted` to true before calling
combineAbortSignals, so the initial `find` scan caught it and we took the
fast path — not the per-iteration check the test was meant to validate.
Switch to an access counter so `aborted` is false on the first read (during
`find`) and true on subsequent reads (inside the loop). This forces the
loop to enter, then catches the flip via the defensive per-iteration check
before any listener is attached to the next input.
Verified the test fails if the per-iteration check is removed.
* fix(lint): include docs/**/*.mjs in the script ESLint block so the AbortController repro passes lint
CI Lint flagged 11 no-undef errors in
docs/verification/abort-controller-refactor/listener-accumulation-repro.mjs
(AbortController, console, process) because the project's flat config
only declared Node globals for ./scripts/**/*.mjs.
The reviewer's suggestion (`/* eslint-env node */`) doesn't work under
ESLint 9 flat config — env directives are deprecated there. The proper
fix is to extend the existing script-globals block to also cover the
verification repro script under docs/.
* fix(core,cli): address PR #4366 critical review findings
Two real bugs the reviewer caught and I confirmed locally:
1. warningHandler.ts didn't actually suppress anything. Adding a
`process.on('warning')` listener does NOT prevent Node's default
onWarning printer from writing to stderr — the default is just an
ordinary listener registered in `lib/internal/process/warning.js`.
My previous code therefore:
- failed to suppress targeted AbortSignal warnings (they still hit
stderr via the default printer)
- produced a SECOND copy of every non-suppressed warning (default
printer + my handler's own stderr.write)
The unit tests missed it because they synthesised a fake warning and
called `process.listeners('warning')` directly rather than going
through `process.emitWarning`.
Fix: snapshot the existing `'warning'` listeners (which include the
default printer and any third-party telemetry hooks) BEFORE replacing
them. Install ours as the sole listener. For non-suppressed warnings
fan out to the captured set so the default printer + telemetry still
fire; for suppressed warnings stop here. Tests now use
`process.emit('warning', ...)` to drive the real listener chain, plus
a spawned-child integration test that asserts the real stderr from
`process.emitWarning` is empty for AbortSignal warnings and still
contains DeprecationWarning text.
2. abortController.createChildAbortController kept a WeakRef to the
child controller. A natural usage pattern — pass `child.signal` into
an async API and drop the controller object — could let the
controller be GC'd while the signal is still in use, after which
`parent.abort()` would no longer propagate. Reproduced with
`node --expose-gc`.
Fix: hold the child strongly via the parent's listener closure. The
reverse-cleanup listener still removes the closure when child aborts
(closure releases child → GC-eligible), and the parent's `{once:true}`
listener self-removes when parent fires (same effect). Net listener
accounting on long-lived parents is unchanged; the only difference is
the controller now stays alive long enough for propagation to reach
downstream consumers that hold only the signal. Tests updated: drop
the old `--expose-gc`-dependent assertion that abandoned children
GC immediately (that was a property of the OLD contract); add a
signal-only-retention test that verifies propagation under the new
contract without needing GC at all.
Verified: 32 helper/warning tests pass (incl. spawned-child stderr
integration); 363 affected caller tests pass; typecheck + prettier +
eslint clean for the touched files.
* fix(core,cli): address PR #4366 review — fix combineAbortSignals orphan listeners + runtime DEBUG toggle
Two real bugs the reviewer caught:
1. combineAbortSignals registered its cleanup listener on
controller.signal AFTER the for-loop. Node does NOT fire 'abort'
listeners added to an already-aborted signal, so when the
per-iteration defensive check aborted the controller mid-loop, the
cleanup never ran — orphaning every input-signal listener registered
before the break, and leaving the (also-registered-after-the-break)
setTimeout uncleared.
Fix: skip timeout scheduling when controller.signal.aborted is
already true post-loop, and when it's true call cleanup()
synchronously instead of registering a doomed listener. Existing
test for the mid-iteration path now also asserts that the
pre-break input signal (a) has zero abort listeners — that's the
assertion that catches the orphan bug. New test for the
already-aborted-input + timeoutMs combination confirms the timer
isn't scheduled (would otherwise overwrite the abort reason).
2. warningHandler captured isDebugMode() in a closure at init time, so
toggling DEBUG / QWEN_DEBUG at runtime (e.g. via a /debug slash
command) didn't update suppression behavior. Moved the check inside
the handler — warnings are rare so the per-emit env-lookup cost is
negligible. New test asserts a mid-stream DEBUG=1 flip starts
forwarding suppressed warnings to the prior-listener chain.
* test(core): strengthen the timeout-guard test in combineAbortSignals to actually exercise the new !aborted check
Reviewer correctly pointed out that the previous version of this test
took the pre-loop fast path (since `a.abort('pre')` ran before
`combineAbortSignals`), so it never reached the in-loop guard at
abortController.ts:138.
Switched to the Proxy `aborted`-getter pattern from the sibling
mid-iteration test (so the loop genuinely re-checks `aborted` and
short-circuits inside the for-loop), and added a `setTimeout` spy that
asserts the timer was never scheduled — this is the only observable
difference from "scheduled then immediately cleared by synchronous
cleanup()", which is what the timer-advance assertion alone couldn't
distinguish.
Verified by mutation testing: removing the guard makes the new test
fail; restoring it makes it pass. Refs PR #4366.
* test(core): cover timeout-triggered cleanup of input-signal listeners in combineAbortSignals
Reviewer noted the timeout path only had an empty-input test, leaving
the leak-sensitive case uncovered: when timeoutMs fires with a
long-lived source signal in the input list, do the input-side
listeners get released? They do (the timeout callback aborts the
combined controller, which fires the auto-cleanup listener registered
on its signal, which calls the per-input removeEventListener), but
that path wasn't tested.
Adds a test that snapshots the source listener count before, asserts
it increased by 1 after combineAbortSignals returns, advances fake
timers past timeoutMs, and asserts the count returns to baseline.
Refs PR #4366.
* fix(test): use pathToFileURL for the warning-handler e2e import on Windows
CI failure on windows-latest:
AssertionError: expected '\r\nnode:internal/modules/run_main:12…'
to match /DeprecationWarning.*Plain deprecation/
Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in:
file, data, and node are supported by the default ESM loader. On
Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'
The e2e test wrote a child script with an `import "<helperPath>"` where
helperPath was a raw Windows absolute path (`D:\a\qwen-code\...`). Node's
ESM loader parses that as a URL on Windows and rejects the `D:` "scheme".
Converted the helper path to a `file://` URL via `pathToFileURL`. macOS
test still passes; the Windows-specific schemes-must-be-URL behavior is
now honored. Refs PR #4366.
* fix(core,cli): address PR #4366 review batch — onAbort leak, migrate missed sites, tighten tests
Adopted 6 of the 7 review threads (skipping the debug-logging suggestion).
1. processFunctionCalls onAbort leak (CRITICAL): the new
`finally { roundAbortController.abort(); }` in _runReasoningLoopInner
would fire the `onAbort` handler in `processFunctionCalls` if
scheduler.schedule or batchDone threw (the explicit
removeEventListener at the old happy-path exit would be skipped),
emitting spurious "Tool call cancelled by user abort." TOOL_RESULT
events for every un-emitted callId — corrupting the transcript and
misleading the model on the next round. Fixed by wrapping schedule
+ batchDone in their own try/finally so removeEventListener always
runs before the outer finally's abort.
2. Migrate 3 new-from-main `new AbortController()` sites that this
PR's audit missed (they came in via the merge from main):
- goals/goalHook.ts (2 sites: judgeController, fallback signal) —
consistency
- hooks/promptHookRunner.ts (1 site: internalAbortController) —
real leak (manual addEventListener without {once:true} or
cleanup, exactly the pattern this PR exists to fix). Switched to
createChildAbortController + finally `internalAbortController.abort()`
for reverse cleanup on the success path.
3. Repro script (`listener-accumulation-repro.mjs`): inlined helper
diverged from production — used WeakRef on child, while production
was changed to strong-ref earlier in this PR. Updated the inlined
copy to match production exactly, with a comment noting the
intentional WeakRef-on-parent-only pattern.
4. warningHandler.ts: documented the snapshot-and-replace trade-offs
in the JSDoc (late-added listeners bypass our filter; late
`removeListener` calls have no effect on our fan-out). Tried the
re-snapshot-per-warning approach the reviewer suggested but it
doesn't work — `removeAllListeners('warning')` permanently removes
the snapshot from Node's tracking, so a `process.listeners('warning')`
filter at fan-out time always returns empty for prior listeners.
The current design is the right trade-off; documentation is the
correct fix.
5. abortController.test.ts: added three coverage gaps the reviewer
identified —
- createChildAbortController forwards custom maxListeners
- manual cleanup() before scheduled timeout fires cancels it
- timeoutMs <= 0 is treated as "no timeout"
6. Migrated `httpHookRunner.ts:202` (the lone caller of the deprecated
`createCombinedAbortSignal`) to `combineAbortSignals` directly,
then deleted `combinedAbortSignal.ts` + its test. All semantics
covered by `combineAbortSignals` tests in abortController.test.ts.
Refreshed `migration-completeness.txt` (now empty — clean grep).
Tests: 194 pass across abortController/warningHandler/agent-runtime/
followup/hooks/goal/promptHook suites. Typecheck + prettier clean.
* docs(verification): commit the headless-scenario scripts referenced by the PR body
The PR body's "End-to-end scenarios I drove locally" section points at
docs/verification/abort-controller-refactor/scripts/02-lite.sh and 06-headless-sigint.sh.
These are the actual reproducible commands behind the EXIT codes /
warning counts reported there — checking them in so anyone can replay
without copy-pasting from the PR description.
Refs PR #4366.
* docs(verification): sync automated-results with current state
Two doc fixes the reviewer flagged:
- migration-completeness.txt was a 0-byte file with a confusing
cross-reference. Populated with the actual grep command + its
"(no output)" result so the empty-output state is explicit.
- automated-results.md still referenced combinedAbortSignal.test.ts (8
tests, @deprecated shim) — both files were deleted in
|
||
|
|
870bdf2a9d
|
feat(cli,sdk): qwen serve daemon (Stage 1) (#3889)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): scaffold `qwen serve` HTTP daemon (Stage 1, #3803) Adds a `serve` subcommand that boots an Express 5 listener with bearer auth, host allowlist, and CORS modeled on `vscode-ide-companion/src/ ide-server.ts`. Ships only `/health` and `/capabilities` to begin with; session/prompt/event routes will land in follow-up PRs once the per- session ACP child-process bridge in `httpAcpBridge.ts` is wired. Defaults to 127.0.0.1 with auth disabled so local development needs no configuration. Binding beyond loopback (e.g. `--hostname 0.0.0.0`) refuses to start without a token (`--token` or `QWEN_SERVER_TOKEN`). Capabilities envelope versioned at v=1 with a `features` array — clients should gate UI off `features`, never off `mode`, so subsequent PRs can add capability tags without breaking older clients. Per design issue's Stage 1 scope (~700-1000 LOC). Adds ~430 LOC of implementation + tests in this scaffold; the remaining budget belongs to the route wiring + bridge implementation in follow-ups. * feat(cli): wire HttpAcpBridge + POST /session for `qwen serve` (#3803) Stage 1 follow-up to the scaffold. Implements the bridge between the HTTP daemon and the existing ACP child agent, plus the first session endpoint. `HttpAcpBridge.spawnOrAttach`: - Spawns `node $cliEntry --acp` per workspace via an injectable `ChannelFactory` (default uses `process.argv[1]`; tests use an in-memory `TransformStream` pair so they don't fork real processes). - Drives the ACP `initialize` + `newSession` handshake via the SDK's `ClientSideConnection`, with a 10s timeout that kills the channel. - Under `sessionScope: 'single'` (default), reuses the live session when the same canonical workspace cwd is requested again — backs the `attached: true` flag. - The `Client` impl on the bridge side proxies file reads/writes to local fs (daemon and agent share the host) and buffers `sessionUpdate` notifications for the SSE wiring in the next PR. `requestPermission` returns `cancelled` until the `/permission/:requestId` route lands. `POST /session`: - 400 on missing or relative `cwd`. - 200 with `{sessionId, workspaceCwd, attached}` on success. - 500 on bridge failure (the failing channel is killed, not leaked). `runQwenServe` constructs the bridge and ties `bridge.shutdown()` into the listener-close path so SIGINT/SIGTERM drain children before the socket closes. Tests (14 new, 0 regressions in the 4967-test baseline): - 9 bridge cases over an in-memory channel — fresh spawn, single-scope reuse, cross-workspace isolation, thread-scope independence, path canonicalization, relative-path rejection, init failure cleanup, init timeout, multi-channel shutdown. - 4 route cases for /session (missing/relative/200/500). - 1 lifecycle case asserting `runQwenServe.close()` calls `bridge.shutdown()` before closing the listener. Verified end-to-end: `qwen serve` boots, `POST /session` spawns a real `qwen --acp` child and returns the SDK-assigned `sessionId`, repeat calls under the same cwd return `attached: true`, `SIGTERM` reaps the child along with the listener. * feat(cli): wire POST /session/:id/prompt + /cancel for `qwen serve` (#3803) Stage 1 follow-up after the bridge scaffold. Adds the two routes a client needs to actually run a turn against the daemon. Bridge: - `sendPrompt(sessionId, req)` looks up the session, FIFO-queues the call against the per-session prompt queue, and forwards through the SDK `ClientSideConnection.prompt`. Concurrent calls observe ACP's "one active prompt per session" invariant — second waits for first. - A failed prompt does NOT poison the queue; the tail catches and keeps draining so the next caller still runs (the original caller still sees its own rejection). - `cancelSession(sessionId, req?)` bypasses the queue and forwards the ACP notification immediately. ACP semantics: the agent winds down the *currently active* prompt; queued work is unaffected. - Both methods throw `SessionNotFoundError` (a typed Error subclass) when the id is unknown so route handlers can map cleanly to 404 without brittle message matching. - Both methods overwrite the `sessionId` field in the request body with the routing id — a stale or spoofed body would otherwise be dispatched to the wrong agent process. Routes: - `POST /session/:id/prompt` → 200 with PromptResponse, 400 on missing/non-array prompt, 404 on unknown session, 500 on agent error. - `POST /session/:id/cancel` → 204 always (cancel is a notification), 404 on unknown session. Tests (14 new — 7 bridge + 7 route, 0 regressions in the 4981 baseline): - sendPrompt: success forwards & returns response · routing-id overrides body sessionId · concurrent prompts FIFO-serialize (verified via per-prompt start/end ordering with a release latch) · failed prompt doesn't block subsequent prompts · 404 for unknown id. - cancelSession: forwards with routing id · 404 for unknown id. - Routes: 200/400/404/500 paths for prompt; 204 with body or empty + 404 for cancel. Verified end-to-end against a real `qwen --acp` child: - POST /session/:id/prompt with `[{type:'text',text:'hi'}]` → 200 `{"stopReason":"end_turn"}` in ~3.4s. - POST /session/:id/cancel → 204. - POST /session/does-not-exist/prompt → 404 with the unknown id surfaced in the body. * feat(cli): wire SSE streaming for `qwen serve` events (#3803) Stage 1 follow-up that turns prompt into a real streaming experience. Replaces the in-memory `notifications: SessionNotification[]` buffer on each session with a per-session EventBus and exposes it through `GET /session/:id/events` as an `text/event-stream` SSE feed. EventBus (`packages/cli/src/serve/eventBus.ts`): - Monotonic per-session ids (`v: 1` schema). Each `publish` chains an id, returning the materialized BridgeEvent. - Bounded ring (default 1000) backs `Last-Event-ID` reconnect — a consumer that drops can resume from `lastEventId` and replay any still-buffered events before live events flow. - Per-subscriber bounded queue (default 256). When a slow consumer overruns its queue, the bus appends a synthetic `client_evicted` terminal frame and closes that subscription so it can't hold the daemon hostage. Other subscribers are unaffected. - `subscribe()` returns an AsyncIterable — registration is synchronous so events `publish`ed immediately after the subscribe land in the queue (a generator-style implementation deferred registration to first `next()` and raced with publishes). - AbortSignal-aware: aborting the signal closes the iterator promptly. Bridge (`httpAcpBridge.ts`): - `BridgeClient.sessionUpdate` now publishes onto the session's EventBus instead of pushing to a plain array — every ACP notification the agent emits becomes a stream event automatically. - New `subscribeEvents(sessionId, opts?)` returns the bus's AsyncIterable; throws `SessionNotFoundError` for unknown ids. - Shutdown closes every live event bus before killing channels so pending consumers unwind cleanly. Route (`server.ts`): - `GET /session/:id/events` sets the SSE content type, advertises a 3s reconnect hint, and writes a 15s heartbeat comment frame to keep proxy/NAT connections alive. - Forwards the `Last-Event-ID` header to the bus. - `req.on('close')` triggers an AbortController that propagates into the bridge subscription so disconnects don't leak subscribers. - 404 when the bridge can't find the session. Capabilities envelope: `STAGE1_FEATURES` now advertises `session_create`, `session_prompt`, `session_cancel`, `session_events` in addition to `health`/`capabilities` so clients can light up UI for the routes that have actually shipped. Tests (16 new, 0 regressions in the 4995 baseline): - 9 EventBus unit cases — id sequencing, live delivery, replay, replay+live splice, fan-out to N subscribers, eviction on overflow, abort-signal unsubscribe, bus.close() drains subscribers, ring-size eviction. - 4 bridge subscribe cases — 404, sessionUpdate→event publishing via real ACP fake-agent, shutdown closes live subscriptions. - 4 SSE route cases against a live HTTP listener — frame format, Last-Event-ID forwarding, 404, abort propagation on disconnect. Verified end-to-end against a real `qwen --acp` child: - Subscribed to `/session/$SID/events`, fired `POST /session/$SID/prompt` with text content. Captured 13 distinct `event: session_update` SSE frames in real time during the model's response — `available_ commands_update` metadata, 9 `agent_thought_chunk` frames carrying the model's chain-of-thought, 3 `agent_message_chunk` frames with the actual reply, and a final usage frame with token totals. - Frames carry monotonic ids 1..13, the daemon-side counter, and are valid SSE per the EventSource spec. * feat(cli): wire POST /permission/:requestId for `qwen serve` (#3803) Stage 1 follow-up that turns `BridgeClient.requestPermission` from a hardcoded `cancelled` placeholder into a real first-responder vote loop, and ships the HTTP route any attached client uses to cast the deciding vote. Bridge: - `requestPermission` generates a UUID requestId, registers a pending entry on a daemon-wide map (and the owning session's `pendingPermissionIds` set), publishes a `permission_request` event onto the session's EventBus (so SSE subscribers see it), and awaits the resolution. - New `respondToPermission(requestId, response)` resolves the pending promise with the supplied outcome. First call wins — subsequent calls return false. On success the bridge publishes a `permission_resolved` event so other attached clients can update their UI when the race is decided. - `cancelSession` and `shutdown` both resolve every still-pending permission for the affected session(s) as `{ outcome: { outcome: 'cancelled' } }` per the ACP spec requirement that a cancelled prompt MUST resolve outstanding requestPermission calls with cancelled. - New `pendingPermissionCount` getter exposes inflight count for inspection / tests. Route (`server.ts`): - `POST /permission/:requestId` validates the body's `outcome` is either `{ outcome: 'cancelled' }` or `{ outcome: 'selected', optionId: string }`, then forwards to `bridge.respondToPermission`. - 200 on accepted vote, 404 when the requestId is unknown or already resolved (Stage 1 doesn't differentiate), 400 on a malformed outcome. Capabilities envelope: STAGE1_FEATURES gains `permission_vote`. Tests (14 new — 9 bridge + 5 route, 0 regressions in the 5011 baseline): - Bridge: publishes permission_request with a generated requestId and waits; respondToPermission first-responder wins; publishes permission_resolved on vote; respondToPermission false for unknown requestId; cancelSession resolves outstanding as cancelled; shutdown resolves outstanding as cancelled. - Route: 200 on selected outcome; 200 on cancelled outcome; 404 on unknown requestId; 400 on malformed outcome; 400 on missing outcome. Verified end-to-end against a real `qwen --acp` child: - Subscribed to /session/$SID/events, sent a prompt asking the agent to write a file at /tmp/qwen-serve-permission-e2e-test.txt. - The agent triggered a permission_request via the bus, surfacing the three options Qwen Code presents (Allow Always / Allow / Reject) with their option ids. - POSTed `{outcome:{outcome:"selected",optionId:"proceed_once"}}` to /permission/$requestId — got HTTP 200. - Bus published the matching permission_resolved event. - Agent proceeded with the writeTextFile tool call; file was actually created on disk with the expected content. * feat(sdk): add DaemonClient for the qwen serve HTTP API (#3803) Stage 1 follow-up that proves the cross-mode protocol-isomorphism design assumption: an SDK client can drive the daemon's HTTP routes end-to-end without going through ProcessTransport's stdio + stream-json path. DaemonClient is a sibling of ProcessTransport, not a replacement. The two speak different protocols (ACP NDJSON over HTTP vs stream-json over stdio). Existing `query()` users keep getting subprocess-mode unchanged; applications that want daemon-mode (cross-client attach, shared MCP pool, network reachability, first-responder permissions) opt in by constructing a DaemonClient against a running `qwen serve`. API surface (`packages/sdk-typescript/src/daemon/`): - `new DaemonClient({ baseUrl, token?, fetch? })`. The `fetch` override is for tests; defaults to `globalThis.fetch`. Trailing slashes on `baseUrl` are stripped. - `health()`, `capabilities()` — discovery. - `createOrAttachSession({ workspaceCwd, modelServiceId? })` — `attached: true` on the response indicates a session was reused under sessionScope:single. - `prompt(sessionId, { prompt: ContentBlock[] })` — returns PromptResult with stopReason. - `cancel(sessionId)` — tolerates 204; throws on 404. - `subscribeEvents(sessionId, { lastEventId?, signal? })` — async iterator over parsed SSE frames; AbortSignal-aware. Native Node AbortController only — jsdom polyfills are incompatible with undici. - `respondToPermission(requestId, response)` — first-responder vote; returns true on 200, false on 404 (lost the race or unknown id), throws on 400/500. `DaemonHttpError` is thrown for any non-2xx (besides the 404 "already-resolved" case on permission votes); carries `status` and `body` so callers can branch on standard daemon HTTP semantics. `parseSseStream(body)` is the underlying SSE parser; exported separately so applications can consume daemon SSE outside the DaemonClient surface. Handles split-chunk frames, comment/retry directives, malformed JSON (skip), trailing frame without final newline. Wire types live SDK-side (no SDK→CLI dep); the capabilities envelope's `v` field signals breaking changes. Tests (26 new, 0 regressions in the 201 baseline): - 7 SSE parser cases — single frame, multiple frames, comments, chunked-split frame, malformed JSON skip, trailing frame on close, empty stream. - 19 DaemonClient cases — health success/error, capabilities, bearer auth presence/absence, createOrAttachSession success/400, prompt body shape + sessionId url-encoding, cancel 204/404, permission 200/400/404, subscribeEvents header forwarding + 404, baseUrl normalization. Verified end-to-end against a real `qwen serve` daemon driving a real `qwen --acp` child: - `client.capabilities()` returned `{v:1, mode:"http-bridge", features: [...7 tags]}`. - First `createOrAttachSession` returned `attached:false`; second returned `attached:true` with the same sessionId. - `client.prompt(...)` with text content yielded `{stopReason: "end_turn"}` while the parallel `subscribeEvents` iterator streamed 10 distinct frames during the same turn. - AbortController on the events iterator cleanly severed the SSE connection. * feat(cli,sdk): list workspace sessions + set session model (#3803) Closes the §04 Stage-1 routes table for `qwen serve` with the two remaining endpoints, plus matching SDK methods. `GET /workspace/:id/sessions` - `:id` is the URL-encoded canonical absolute workspace path (Express decodes path params automatically; clients pass `encodeURIComponent(cwd)`). - Returns `{ sessions: [{ sessionId, workspaceCwd }, ...] }` for live sessions whose canonical workspace matches. - Empty array (not 404) when the workspace is idle so picker UIs don't have to special-case "no sessions yet". - 400 when the decoded path isn't absolute. `POST /session/:id/model` - Body: `{ modelId: string, ... }`. The route's `:id` overrides any spoofed sessionId in the body. - Forwards to ACP's `unstable_setSessionModel` and publishes a `model_switched` event onto the session bus so cross-client UIs update. - 200 with the agent's response on success, 400 on missing/empty modelId, 404 on unknown session. - The SDK method is currently unstable; documented in the bridge comment in case the spec renames the method when it stabilizes. Bridge: - New `listWorkspaceSessions(workspaceCwd)` iterates `byId.values()` and filters by canonical workspace path; works for both `single` and `thread` session scopes. - New `setSessionModel(sessionId, req)` forwards through `connection.unstable_setSessionModel`, normalizes sessionId, publishes `model_switched`, throws SessionNotFoundError on unknown ids. `STAGE1_FEATURES` capabilities envelope grows to 9 tags, adding `session_list` and `session_set_model`. SDK (`DaemonClient`): - `listWorkspaceSessions(workspaceCwd)` URL-encodes the cwd and returns the parsed `sessions` array directly. - `setSessionModel(sessionId, modelId)` POSTs the body and returns the agent response (currently opaque per ACP unstable spec). - Wire types `DaemonSessionSummary` and `SetModelResult` exported from the SDK barrel. Tangential cleanup: `sendBridgeError` now extracts a useful message from non-Error values via a small `errorMessage` helper. JSON-RPC errors from the agent (`{code, message, data}`) used to surface as `"[object Object]"` in the 500 response body; they now show the inner `message` field. Caught while running the model-set e2e. Tests (17 new — 9 bridge + 7 route + 4 SDK, 0 regressions in the 5022 + 227 baselines): - Bridge listWorkspaceSessions: matching cwd returns the live sessions; canonicalizes the lookup; empty for relative paths. - Bridge setSessionModel: forwards modelId + overrides body sessionId; publishes model_switched event; 404 unknown session. - Route /workspace/:id/sessions: returns the bridge list; empty for idle workspace; 400 for relative path. - Route /session/:id/model: 200 success; 400 missing modelId; 400 empty modelId; 404 unknown session. - SDK listWorkspaceSessions: URL-encodes the cwd; throws on 400. - SDK setSessionModel: posts body; throws on 404. Verified end-to-end against a real `qwen serve`: - SDK reports 9 capability features, list returns the existing session, attached:true on repeat create, and `setSessionModel` rejects with HTTP 500 when the modelId isn't registered (with the daemon now surfacing "Internal error" instead of "[object Object]"). - 404 path through SDK on unknown sessionId works. * fix(cli,sdk): audit round 1 follow-ups for `qwen serve` (#3803) Self-review pass on PR #3889. Two real correctness bugs and an ergonomics gap, plus the test-coverage holes the audit surfaced. The loudest finding ("host allowlist no-op when bind=localhost") was a false positive — the conditional was misread; existing tests already prove the validator is active on `localhost` binds. Real fixes: - Bearer-auth timing-attack: `parts[1] !== token` short-circuits per byte, leaking which prefix is correct via response latency. Replace with SHA-256 of both sides + `crypto.timingSafeEqual` so comparison is constant-time regardless of token length. - Concurrent `spawnOrAttach` race in single-scope: two parallel callers for the same workspace both passed the `byWorkspace.get` check, both spawned, and one entry ended up orphaned in `byId` while the other won `byWorkspace`. Violates the "at most one session per workspace" invariant. Coalesce via an `inFlightSpawns` map: parallel callers attach to the in-flight promise and report `attached: true`. The slot is cleared on both success and rejection so a failed spawn doesn't poison the workspace forever. New test asserts ONE channel spawns under parallel calls and that retry works after rejection. - `Number.parseInt('1.5e10z', 10)` returns 1, so a malformed `Last-Event-ID` header silently passes through. Tighten `parseLastEventId` to `^\d+$` so anything not a pure decimal integer is dropped. New test exercises 'abc', '-1', '1.5e10z'. Ergonomics: - `LOOPBACK_BINDS` and `LOOPBACK_HOST_BINDS` now include `::1` and `[::1]`. IPv6 loopback users no longer have to set a token. Host-allowlist allows `[::1]:port` Host headers. Documentation: - `BridgeClient` doc-comment now states the Stage 1 trust model explicitly: agent runs as the same UID, the file-proxy methods are NOT a workspace-cwd sandbox, restricting them would be theatre. The audit flagged this as a "design gap" but the daemon-and-agent-on-same-host posture makes a sandbox here redundant — Stage 4+ remote-sandbox swaps the Client for a sandbox-aware variant. SDK fix: - `DaemonClient.failOnError` previously called `res.json()`, which consumes the body even on parse-failure; the subsequent `res.text()` returned empty. New impl reads once as text and attempts JSON-parse; raw text is the fallback. New test asserts a `text/plain` 502 surfaces the body verbatim. Test gap fills (audit-flagged): - Bridge: in-memory file-proxy tests for `BridgeClient.{read,write} TextFile` including line/limit slicing. - SSE route: `stream_error` synthetic frame on iterator throw mid-stream; numeric Last-Event-ID forwarded; malformed Last-Event-ID dropped. - DaemonClient: text/plain error body coerced to `body` field; `respondToPermission` 5xx throws; `subscribeEvents` null-body throws; `cancel`/`respondToPermission` URL-encode session/request ids that contain slashes. Verified end-to-end with a token-required daemon: right token → 200, wrong/missing/malformed → 401. All paths return uniform 401 messages so a side-channel can't distinguish between "no header", "bad scheme", and "wrong token". Test counts: cli serve **89** (was 81, +8), sdk daemon **35** (was 30, +5). Full suites still green. * fix(cli): audit round 2 follow-ups for `qwen serve` (#3803) Second self-review pass on PR #3889. Three real bugs (one correctness, one resource-cleanup, one cosmetic) plus consolidation of the loopback bindings into a single source of truth. Real fixes: - Shutdown could hang forever on a long-lived SSE consumer: `server.close` waits for every in-flight connection to drain, and a paused EventSource client never disconnects. Added a `SHUTDOWN_FORCE_CLOSE_MS` (5s) timer that calls `server.closeAllConnections()` to force-destroy stuck sockets, then resolves so `process.exit(0)` can run. New test asserts close completes well under 5.5s even when an SSE GET is in flight. - Signal-handler race during shutdown: round 1 detached the SIGINT/SIGTERM listeners *up front* in `handle.close()`. If a second SIGTERM arrived during the drain, no handler existed and Node's default termination ran, orphaning agent children. Switch to detaching at the *end* of the close path (in `finish()`): during the drain window the handler is still attached and the `if (shuttingDown) return` guard makes a second signal a no-op; after drain completes we can safely remove the listeners (this also fixes a test-suite MaxListenersExceededWarning that fired once we ran the runQwenServe tests >10 times in a single process). - SSE response had no `error` listener. When the underlying TCP socket died (RST, kill -9 on the client), the next `res.write` threw EPIPE and Express forwarded it to the default error handler, logging noisily. Added `res.on('error', cleanup)` so the failure is absorbed and triggers the same teardown path the `req.on('close')` handler uses. Validation: - `createHttpAcpBridge` now throws on invalid `sessionScope` (anything other than `'single'` or `'thread'`) and on `initializeTimeoutMs <= 0`. Misconfigured callers used to silently degrade to thread behavior; now they fail loudly. Cleanup: - The `LOOPBACK_BINDS` set was duplicated between `auth.ts` and `runQwenServe.ts` (round 1 missed this). Extracted into `packages/cli/src/serve/loopbackBinds.ts` with a single `isLoopbackBind(hostname)` helper. Both files now import; drift is impossible. - `res.flushHeaders?.()` lost the optional chaining. The method is on `http.ServerResponse` since Node 1.6; our `engines` floor is 20. Tests added: - bridge: `sessionScope` validation, `initializeTimeoutMs` validation. - server: shutdown force-close timeout, SIGINT/SIGTERM listener detach-after-drain. False positives from the round 2 audit (verified and dismissed): - "EventBus nextId overflow at 2^53" — theoretical only (would require ~9 quadrillion publishes per session). No code change. - "Subscribe-during-close race" — JS is single-threaded; the close() flag is set synchronously before the loop touches state. - "Queued prompts on shutdown" — by design; documented via the promptQueue tail comment. - "10MB body parser limit" — design choice for Stage 1's in-memory buffering model; revisit if ACP streaming lands in Stage 2. - "Unbounded body read in DaemonClient.failOnError" — daemon is local in Stage 1; the threat surface for adversarial-large error bodies is the same as the daemon's other unbounded buffers. Test counts: cli serve **93** (was 89, +4), full cli **5047** (no regressions), sdk **236** (no regressions). * docs(cli): audit rounds 3 + 4 follow-ups for `qwen serve` (#3803) Two more self-review passes on PR #3889. No correctness bugs surfaced this time — round 3 found a HIGH-severity Windows-path claim that turned out to be a false positive (`path.win32.isAbsolute('/foo/bar')` returns true; verified against Node 20). Round 4 confirmed every prior decision and surfaced one latent-but-not-currently-triggered concurrency note. Changes are pure documentation + a tiny optional-chain cleanup: - Drop `?.` on `server.closeAllConnections()` in runQwenServe.ts — the method exists since Node 18.2 and our `engines` floor is 20. The optional chain dated from before round 2's force-close timer landed; clean it up. - Help text for `qwen serve --port` now documents that port 0 means "OS-assigned ephemeral port" (which the implementation has always supported but never advertised). - `defaultSpawnChannelFactory` gains a comment near the spawn site documenting the FD-budget implication (~3 FDs per session, bump `ulimit -n` for many concurrent sessions) and the `stdio: ['pipe', 'pipe', 'inherit']` choice (child stderr lands in the daemon's stderr, interleaved across sessions). Both are Stage-1-accepted; Stage 2/4+ revisit each. - Comment on the bridge's `byWorkspace`/`byId` Maps documenting the known gap that a child crashing between requests leaves a garbage SessionEntry until daemon shutdown — surfaced as a per-prompt failure when the dead session is touched, not a hang. Stage 2's in-process bridge eliminates the spawned-child failure mode entirely so this gap goes away naturally. - `EventBus.subscribe` doc-comment now states explicitly that the returned iterator is NOT safe to drive from concurrent `.next()` callers — the underlying queue isn't atomic. Daemon usage is the sequential `for await ... of` inside the SSE route, so this is safe in production. Documented so a future fan-out consumer doesn't accidentally rely on undefined behavior. False positives verified and dismissed (round 3 + 4 combined): - `path.isAbsolute('/foo/bar')` Windows breakage — `path.win32. isAbsolute('/foo/bar')` is true; verified empirically. - "Windows drive divergence" causing duplicate sessions — different drives are different on-disk paths; sessions intentionally differ. - "parseSseStream early-break leaks reader" — `for await ... break` triggers `iterator.return()` which runs the generator's `finally` that calls `releaseLock`. Standard JS semantics. - "Promise executor sync-throw fragility in requestPermission" — sync throws inside `new Promise(executor)` reject the outer promise; functionally correct, just stylistic. - "Force-close timeout test elapsed assertion flakiness" — assertion is `< 5500ms` but the natural happy-path is sub-100ms. Generous headroom; not flake-prone in practice. - "fetch reference stale after polyfill" — `globalThis.fetch.bind` captures at construction; tests inject `opts.fetch` instead of polyfilling, which is the correct pattern. Test counts unchanged (cli serve **93**, sdk **236**); typecheck + lint clean. STAGE1_FEATURES still matches every implemented route 1:1, fakeBridge in tests implements every HttpAcpBridge method. * fix(cli): PR #3889 review round 1 — critical correctness (#3803) Addresses the four critical findings from the PR #3889 reviewer pass: 1. ACP `ReadTextFileRequest.line` is 1-based per spec, but the bridge's `BridgeClient.readTextFile` was treating it as a 0-based slice index. A client asking for `{line:1, limit:2}` ("first two lines") was getting lines 2-3 — a sign-off-by-one bug that breaks every editor / SDK client following the ACP schema. Convert to 0-based via `Math.max(0, line - 1)`. The existing slice test was asserting the wrong behavior; updated to expect the spec-correct result and added a second `line:3, limit:2` case to lock in the offset. 2. `modelServiceId` was accepted by the SDK + server `POST /session` path, forwarded into `bridge.spawnOrAttach`, and then silently dropped: `doSpawn` never wired it into the agent. Callers requesting a specific model got the agent's default and no indication anything was wrong. Now `doSpawn` issues `unstable_setSessionModel` immediately after `newSession`. If the agent rejects the model id, the half-initialized session is torn down and the spawn rejects so the caller can retry cleanly instead of inheriting silent drift. Three new bridge tests: happy path, omit-when-undefined, agent-rejection cleanup. 3. The CORS middleware used `cors({ origin: (o, cb) => cb(new CORSError(...), false) })` for browser-Origin requests. `cors` flows the Error into Express's error chain; without an explicit error handler that produces a 500 + HTML body, which is misleading for what is really a deterministic 403 denial. Replace with a tiny `RequestHandler` that checks `req.headers.origin` directly and returns `403 { error: 'Request denied by CORS policy' }` JSON. Drops the `cors` and `@types/cors` dependencies — there's no other consumer in the cli package. 4. The SSE `stream_error` synthetic frame hard-coded `id: 0`, which would regress the client's `Last-Event-ID` tracker and trigger duplicate replays on reconnect. The frame is terminal and daemon-emitted — it has no place in the per-session monotonic sequence. Refactor `formatSseFrame` to omit the `id:` line when the input event has no id field, and emit `stream_error` without one. Test updated to assert `frames[1].id === undefined` while the preceding `session_update` still carries its monotonic id. Tangential cleanup: `errorMessage` now formats the SSE error body (was `err.message` only — would have shown `[object Object]` for JSON-RPC errors mid-stream, mirroring the round-1 SDK fix). Test counts: cli serve **96** (was 93, +3 modelServiceId cases); existing readTextFile slice test rewritten in place. Full typecheck + lint + suite green. * fix(cli,sdk): PR #3889 review round 2 — SSE robustness + EventBus polish (#3803) Second batch of reviewer-flagged fixes for PR #3889. Addresses 7 robustness issues across the daemon's SSE pipeline + the bus + the SDK's stream parser. Daemon SSE (`server.ts`): - SSE writes now respect backpressure. `res.write` returns false when the kernel send buffer is full; the previous code ignored that and Node accumulated payloads in user-space memory unboundedly. A slow consumer on a chatty session could balloon daemon RSS. New `writeWithBackpressure` helper awaits `drain` (or `close`/`error`) before scheduling the next write — for both per-frame writes and heartbeats. - `parseLastEventId` rejects values > `Number.MAX_SAFE_INTEGER`. With the prior `^\d+$` regex a malicious 25-digit value would parse to a number that loses precision and confuses replay comparisons. EventBus (`eventBus.ts`): - `Last-Event-ID` replay events now `forcePush` past `maxQueued`. A client reconnecting with a 1000-event gap on a subscriber whose cap is 256 was silently losing entries 257-1000 — a sign-off-by- nothing breakage of the resume contract. Live publishes still go through the normal cap (slow live consumer must be evictable); historical replay is bypassed. - `onAbort` now disposes the subscription immediately instead of only closing the queue. An aborted-but-never-iterated subscriber used to linger in `bus.subs` until the consumer drove `next()` / `return()`. New tests cover both abort-after-subscribe and already-aborted-at-subscribe paths. - `BoundedAsyncQueue.next` now checks `buf.length > 0` before shifting instead of `buf.shift() !== undefined`. The bus never pushes `undefined` today but the queue is generic — the prior pattern would mis-handle a queue whose element type legitimately includes undefined. SDK SSE parser (`sse.ts`): - Now flushes the TextDecoder on stream close. Without the final `decoder.decode()`, an incomplete multi-byte UTF-8 sequence at the tail of the last chunk was silently dropped — corrupting any frame whose JSON ended mid-character. New test feeds a stream split mid-byte through "中" (3-byte UTF-8) and asserts the character round-trips. - Frame separators now accept both `\n\n` and `\r\n\r\n`. SSE spec allows CRLF, and intermediaries (corporate proxies, some Node http servers) sometimes normalize. Frame field splitter also accepts `\r?\n`. Two new tests cover pure CRLF + mixed-LF/CRLF. Test counts: cli serve **99** (was 96, +3 EventBus); sdk daemon-sse **10** (was 7, +3). Full typecheck + lint + suite green. * docs(cli,sdk): PR #3889 review round 3 — minor + docs (#3803) Last batch from the PR #3889 reviewer pass: mostly docs + a ReDoS-tooling-silencing rewrite + a yargs-key cleanup. - `commands/serve.ts` ServeArgs interface dropped the camelCase `httpBridge` mirror; the handler now reads `argv['http-bridge']` matching the declared option name. The dual surface relied on yargs's camelCase expansion behavior — fragile if yargs config ever changes. - `DaemonClient` constructor's `baseUrl.replace(/\/+$/, '')` (which is end-anchored and linear, but CodeQL's polynomial-regex detector flags any `\/+$` pattern on attacker-controlled input) swapped for a hand-rolled `stripTrailingSlashes` loop. Same behavior, no rule trigger. - `defaultSpawnChannelFactory`'s `cwd: workspaceCwd` flow into `spawn` is the second CodeQL finding ("uncontrolled data used in path expression"). It IS user-controlled, by design — that's the Stage 1 trust model. Added a `// lgtm[js/shell-command- constructed-from-input]` suppression with a comment explaining the model and pointing at issue #3803 §11 for the Stage 4+ remote- sandbox replacement. - Stale doc comment on `createServeApp` that still listed only `/health`, `/capabilities`, `POST /session` as shipped — now enumerates all 9 routes that match §04 of the design. - Stale doc comment on `HttpAcpBridge` saying "Stage 1 buffers them in-memory; SSE wiring lands in the next PR" — SSE wiring landed in commit |
||
|
|
3b0b6c052b
|
feat(cli): add API preconnect to reduce first-call latency (#3318)
Fire a fire-and-forget HEAD request early in startup to warm the TCP+TLS connection. Subsequent SDK calls share an undici dispatcher with preconnect, reusing the warmed connection to save 100-200ms on the first request. Skip conditions: - NODE_EXTRA_CA_CERTS set (enterprise TLS inspection) - Sandbox mode (process-restart context) - Non-default baseUrl (mTLS / private deployment) - Non-Node runtimes (Bun) Disable via QWEN_CODE_DISABLE_PRECONNECT=1. Closes #3223 |
||
|
|
5a43efcae4
|
fix(editor): detect Zed.app on macOS when CLI is not in PATH (#3303)
* fix(editor): detect Zed.app on macOS when CLI is not in PATH On macOS, Zed editor is typically installed via Homebrew or direct download, but the CLI command 'zed' is not automatically added to PATH. This fix adds detection for Zed.app bundle at: - /Applications/Zed.app - ~/Applications/Zed.app When the CLI is not found but the app bundle exists, the code now falls back to using the CLI inside the app bundle at Contents/MacOS/zed. Fixes #3287 * fix(editor): use shared getEditorExecutable in useLaunchEditor - Export getEditorExecutable() from editor.ts for use by both getDiffCommand and useLaunchEditor - Updated useLaunchEditor.ts to use getEditorExecutable instead of its own implementation - Updated tests to be platform-agnostic for macOS app bundle path testing - Fixes: Zed on macOS is now detected when installed via app bundle (not just CLI in PATH) * fix(editor): use correct Zed CLI path (Contents/MacOS/cli) - Changed from Contents/MacOS/zed (GUI binary) to Contents/MacOS/cli (actual CLI) - The GUI binary does not support --wait/--diff flags - Updated tests to verify correct CLI path with regex matching for cross-platform * style(editor): fix prettier trailing whitespace issues Trailing spaces and array line-wrapping in zed macOS detection code. * fix(editor): return null when editor not found + remove unused var - getEditorExecutable now returns null (not fallback string) so useLaunchEditor error handling actually works - remove unused getAppBundleCliPath in test file (typecheck fix) * fix: add vitest globals to eslint config for test files * fix: remove duplicate empty test with orphan toEqual call * fix: resolve ESLint errors in editor.test.ts (arrow-body-style, no-useless-escape) * chore: remove debug script check_braces.js * fix: sync checkHasEditorType with getEditorExecutable, remove pr-body.md - Replace zedAppExists() check in checkHasEditorType with getEditorExecutable() !== null, keeping availability detection and execution in sync (fixes partial install false positive) - Remove unused zedAppExists() function - Remove scratch file pr-body.md * fix(editor): defer os.homedir() call to avoid breaking tests with incomplete node:os mocks The zedMacOsPaths constant was calling homedir() at module initialization time, which caused 'homedir is not a function' errors in CLI test files (systemInfo.test.ts, shellCommandProcessor.test.ts) that mock node:os without providing a homedir mock. Fix: convert zedMacOsPaths from a constant to a lazy function getZedAppPaths() that computes the paths only when called. --------- Co-authored-by: lamb <906276457@qq.com> |
||
|
|
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 <qwen-coder@alibabacloud.com> |
||
|
|
8753245b5f |
feat(channels): add DM pairing flow for sender approval
- Add PairingStore for managing pending requests and approved users - Update SenderGate to support pairing policy with code generation - Add CLI commands: `qwen channel pairing list/approve` - Document pairing flow with rules and usage examples This allows unknown senders to request access via a pairing code that the bot operator approves through the CLI. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
74b342623c | Merge branch 'main' into feature/arena-agent-collaboration | ||
|
|
6258ee53a1 |
chore: Fix lint errors in web-templates package
- Remove unused npmCommand variable from build.mjs - Add no-undef and .mjs file support to web-templates eslint config Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
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 <qwen-coder@alibabacloud.com> |
||
|
|
6b55c8161f |
feat(arena): Add agent collaboration arena feature
Introduces a new Arena system for running multiple AI agents in parallel terminal sessions with support for iTerm and Tmux backends. Core: - Add ArenaManager and ArenaAgentClient for orchestrating multi-agent sessions - Add terminal backends (ITermBackend, TmuxBackend) with feature detection - Add git worktree service for isolated agent workspaces - Add arena event system for real-time status updates CLI: - Add /arena command with start, stop, status, and select subcommands - Add Arena dialogs (Select, Start, Status, Stop) - Add ArenaCards component for displaying parallel agent outputs - Consolidate message components into StatusMessages and ConversationMessages - Add MultiSelect component for agent selection Config: - Add arena-related settings to schema and config Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
c243f1d90d | chore: update ESLint configuration and lint-staged command | ||
|
|
22aa6656a4 | feat: add new insight page with Vite setup | ||
|
|
4abec5c055 | Merge remote-tracking branch 'origin/main' into feat/debug-logging-refactor | ||
|
|
86a43618a7 |
refactor(export): built-in package for assets export
1. use built-in assets package to support html export 2. improve markdown export |
||
|
|
7f4579ee6a |
refactor(debug): add ESLint no-console rule with targeted allowlist
- Add 'no-console': 'error' to main source files (packages/core/src, packages/cli/src) - Configure targeted allowlist for: - Test files and scripts - VS Code extension (out of scope) - WebUI package (UI component library) - Integration tests - Intentionally excluded files (acpAgent.ts, stdioHelpers.ts) - Consolidate no-console overrides into a single clearly-marked section - Enforced in CI via npm run lint:ci Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
340becefb4 |
refactor(lsp): migrate remaining LSP console calls to debugLogger
- LspConfigLoader.ts: 7 console.warn calls → debugLogger.warn - LspConnectionFactory.ts: 1 console.warn call → debugLogger.warn - Add eslint no-console overrides for test files and scripts - Completes LSP subsystem migration (8 more calls) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
02db22dd78 |
chore: update eslint config and translation adjustments
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> 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 |
||
|
|
8d2d29a422 | feat(webui): Add UMD build format and CDN usage support | ||
|
|
73b4a9b560 | chore(vscode-ide-companion, webui): update plan md | ||
|
|
af76450dee | feat(webui): Infrastructure Setup (Prerequisites) | ||
|
|
e5dbd69899 | feat: fix ci | ||
|
|
53486b7fd3 | Merge branch 'main' into docs-byYijing | ||
|
|
e12a80b24e | feat: update docs | ||
|
|
111234eb24 |
refactor(vscode-ide-companion): simplify ACP connection and cleanup configurations
- Remove .claude from .gitignore - Update CSS file path in eslint config - Simplify VS Code extension title - Remove unused keybinding for openChat command - Delete unused auth constants file - Simplify ACP connection by removing backend parameter - Move authMethod to acpTypes - Restrict ACP backend to Qwen only - Remove backend property from connection state - Minor formatting update in webview index.tsx |
||
|
|
e47263f7c9 | Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/jinjing/write-and-read-file-in-vscode | ||
|
|
3056f8a63d |
feat(tests): move SDK integration tests to integration-tests to share globalSetup
|
||
|
|
56957a687b | refactor: rename ambiguous exported types | ||
|
|
769a438fa4 |
feat: enhance logging capabilities and update query options in sdk-typescript
- Introduced a new logging system with adjustable log levels (debug, info, warn, error). - Updated query options to include a logLevel parameter for controlling verbosity. - Refactored existing code to utilize the new logging system for better error handling and debugging. - Cleaned up unused code and improved the structure of the SDK. |
||
|
|
e1ffaec499 | feat: create draft framework for cli & sdk | ||
|
|
90fc53a9df | refactor(webview): Refactoring Input Form and Timeline Components | ||
|
|
c038745897 |
fix(vscode-ide-companion): 修复 Tailwind 可重用组件类和 ESLint 配置, 调整 ChatHeader 按钮样式
- 在 tailwind.css 中正确定义可重用的 Tailwind 组件类 - 修复 ChatHeader 组件中的按钮样式,确保 hover 效果正常工作 - 修复 ESLint 配置中的 importPlugin 导入问题 - 清理 App.css 中重复的 CSS 变量定义 - 为 btn-ghost 类设置 4px border radius - 为按钮内的 span 添加左右 4px padding (使用 px-1) - 确保按钮 hover 时有背景色效果 |
||
|
|
dc40995e70 | feat(vscode-ide-companion): import chat chat customEditor to vscode extension folder | ||
|
|
be633a80cc
|
📦 Release qwen-code CLI as a Standalone Bundled Package (#866) | ||
|
|
eb95c131be
|
Sync upstream Gemini-CLI v0.8.2 (#838) | ||
|
|
dbdb4db4f0 | fix: lockfile and configs | ||
|
|
f19789b381 | fix: lint error | ||
|
|
14ea33063f | Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0 | ||
|
|
bfef867ba7
|
Add a2a-server package to gemini-cli (#6597) | ||
|
|
0f031a7f89
|
Explict imports & exports with type modifier (#3774)
|
||
|
|
ee4feea006
|
chore: consistently import node modules with prefix (#3013)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com> |
||
|
|
49cce8a15d
|
chore(test): install and configure vitest eslint plugin (#3228)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com> |
||
|
|
ef46d64ae5
|
Fix(grep): memory overflow in grep search and enhance test coverage (#5911)
Co-authored-by: Jacob Richman <jacob314@gmail.com> |
||
|
|
714b3dab73
|
chore(lint config): add test-utils to eslint config (#6768) |