Skip to content

fix(lsp): 修复 LSP 文档、isPathSafe 限制,并提升 LSP 工具调用率#117

Open
BingqingLyu wants to merge 28 commits into
mainfrom
pr-3615-worktree-fix-lsp-docs-and-bugs
Open

fix(lsp): 修复 LSP 文档、isPathSafe 限制,并提升 LSP 工具调用率#117
BingqingLyu wants to merge 28 commits into
mainfrom
pr-3615-worktree-fix-lsp-docs-and-bugs

Conversation

@BingqingLyu

@BingqingLyu BingqingLyu commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

修复 LSP 功能的多个文档和代码问题,提升模型对 LSP 工具的调用率。

变更内容

  1. 文档修复 — 删除了过时/不存在的引用:

    • 删除不存在的 packages/cli/LSP_DEBUGGING_GUIDE.md 引用
    • 删除未实现的 /lsp status slash command 引用
    • 修正 debug 方法说明(DEBUG=lsp*~/.qwen/debug/ session 日志)
    • 删除 code.claude.com 外部链接
    • 添加 clangd 专项 troubleshooting(--background-indexcompile_commands.json
  2. isPathSafe 修复 — 允许 .lsp.json 中使用绝对路径的语言服务器命令:

    • 之前:/usr/bin/clangd 等 workspace 外的绝对路径被安全检查拦截,服务器静默启动失败
    • 之后:绝对路径直接放行(已有 workspace trust 机制保护),仅拦截相对路径越界
  3. 系统提示词 LSP 优先指令 — 当 LSP 启用时,在系统提示词顶部注入 LSP 优先使用指令,引导模型优先调用 LSP 工具

  4. symbolName 参数 — LSP 工具支持通过符号名查找,不再强制要求 filePath + line + character:

    • 模型可以直接调用 {operation: "findReferences", symbolName: "Calculator"}
    • 工具内部自动通过 workspaceSymbol 解析符号位置
  5. Grep/ReadFile 工具描述提醒 — 当 LSP 启用时,在 grep 和 readfile 的工具描述中加入 LSP 优先提醒

Demo 项目

LSP 测试示例项目(C++/TypeScript/Java/Python):https://github.com/yiliang114/qwen-code-lsp-demo

Validation

Unit Tests

NativeLspService.test.ts:    14/14 passed ✅
LspConfigLoader.test.ts:      6/6  passed ✅
lsp.test.ts:                 69/69 passed ✅
client.test.ts:              63/63 passed ✅
prompts.test.ts:             53/53 passed ✅

Manual LSP Test (C++ / clangd)

Operation 状态 LSP 输出
goToDefinition include/calculator.h:13:7 [clangd]
workspaceSymbol Calculator (Class) in geometry + 6 references [clangd]
hover class Calculator
findReferences findReferences(area) via symbolName
goToImplementation goToDefinition(Shape) via symbolName
diagnostics No errors (clean code)
documentSymbol main (Function), using namespace geometry

Debug 日志

$ grep '[LSP]' ~/.qwen/debug/latest
2026-04-25T10:42:20.632Z [INFO] [LSP] LSP server clangd started successfully

Scope / Risk

  • Risk: Low — 文档修复 + 安全检查放宽(有 trust 机制兜底)+ 提示词优化
  • isPathSafe: 绝对路径放行,相对路径越界仍被拦截
  • LSP 调用率: 受模型 tool selection 能力影响,不保证 100%,但工程层面已尽力优化

Testing Matrix

🍏 macOS 🐧 Linux 🪟 Windows
clangd ⚠️ ⚠️
tsserver ⚠️ ⚠️
jdtls ⚠️ ⚠️
pylsp ⚠️ ⚠️

zhangxy-zju and others added 28 commits April 23, 2026 20:02
…#3559)

params.pages !== undefined let "" fall through to parsePDFPageRange(''),
which returns null and surfaced "Invalid pages parameter: ''" for every
read_file call from models that default optional strings to "".

Switch to a truthy check so "" behaves the same as an omitted field, and
add a regression test.

Fixes QwenLM#3558
…QwenLM#3540)

* feat(session): auto-title sessions via fast model, add /rename --auto

The /rename work in QwenLM#3093 generates kebab-case titles only when the user
explicitly runs `/rename` with no args; until they do, the session picker
shows the first user prompt (often truncated or misleading). This change
adds a sentence-case auto-title that fires once per session after the
first assistant turn, using the configured fast model.

New service: `packages/core/src/services/sessionTitle.ts` —
`tryGenerateSessionTitle(config, signal)` returns a discriminated outcome
(`{ok: true, title, modelUsed}` | `{ok: false, reason}`) so callers can
either handle failures generically or map reasons to actionable messages.
Prompt shape: 3-7 words, sentence case, good/bad examples including a
CJK row, JSON schema enforced via `baseLlmClient.generateJson`.
`maxAttempts: 1` — titles are cosmetic metadata and shouldn't fight
rate limits.

Trigger point: `ChatRecordingService.maybeTriggerAutoTitle` runs after
`recordAssistantTurn`. Fire-and-forget promise, guarded by:

- `currentCustomTitle` — don't overwrite any existing title.
- `autoTitleController` doubles as in-flight flag; a second turn while
  the first is still pending is a no-op.
- `autoTitleAttempts` cap of 3 — the first assistant turn may be a
  pure tool-call with no user-visible text; retry for a handful of
  turns until a title lands. Cap bounds total waste.
- `!config.isInteractive()` — headless CLI (`qwen -p`, CI) never auto-
  titles; spending fast-model tokens on a one-shot session is waste.
- `autoTitleDisabledByEnv()` — `QWEN_DISABLE_AUTO_TITLE=1` opt-out.
- `config.getFastModel()` falsy — skip entirely rather than falling
  back to the main model; auto-titling on main-model tokens is too
  expensive to be silent.

Persistence: `CustomTitleRecordPayload` grows a `titleSource: 'auto' |
'manual'` field. Absent on pre-change records (treated as `undefined`
→ manual, safe default so a user's pre-upgrade `/rename` is never
silently reclassified). `SessionPicker` renders `titleSource === 'auto'`
titles in dim (secondary) color; manual stays full contrast. On resume,
the persisted source is rehydrated into `currentTitleSource` — without
this, finalize's re-append would rewrite an auto title as manual on
every resume cycle.

Cross-process manual-rename guard: when two CLI tabs target the same
JSONL, in-memory state can diverge. Before writing an auto record, the
IIFE re-reads the file via `sessionService.getSessionTitleInfo`. If a
`/rename` from another process landed as manual, bail and sync local
state — never clobber a deliberately-chosen manual title with a model
guess. Cost is one 64KB tail read per successful generation.

`finalize()` aborts the in-flight controller before re-appending the
title record. Session switch / shutdown doesn't have to wait on a slow
fast-model call.

New user-facing command: `/rename --auto` regenerates via the same
generator — explicit user trigger, overwrites whatever's there (manual
or auto) because the user asked. Errors route through
`autoFailureMessage(reason)` so `empty_history`, `model_error`,
`aborted`, etc. each get actionable guidance rather than a generic
"could not generate". `/rename -- --literal-name` is the sentinel for
titles that start with `--`; unknown `--flag` tokens error with a hint
pointing at the sentinel. Existing `/rename <name>` and bare `/rename`
(kebab-case via existing path) are unchanged, except the kebab path now
prefers fast model when available and runs its output through
`stripTerminalControlSequences` (same ANSI/OSC-8 hardening as the
sentence-case path).

New shared util: `packages/core/src/utils/terminalSafe.ts` —
`stripTerminalControlSequences(s)` strips OSC (\x1b]...\x07|\x1b\\), CSI
(\x1b[...[a-zA-Z]), SS2/SS3 leaders, and C0/C1/DEL as a backstop. A
model-returned `\x1b[2J` or OSC-8 hyperlink escape would otherwise
execute on every SessionPicker render; both sentence-case and kebab
paths now route titles through the helper before they reach the JSONL
or the UI.

Tail-read extractor: `extractLastJsonStringFields(text, primaryKey,
otherKeys, lineContains)` reads multiple fields from the same matching
line in a single pass. Two separate tail scans could return a mismatched
pair (primary from a newer record, secondary from an older one with only
the primary set); the new helper guarantees the pair is atomic. Validates
a proper closing quote on the primary value so a crash-truncated trailing
record can't win the latest-match race. `readLastJsonStringFieldsSync`
is its file-reading wrapper — same tail-window fast path and full-file
fallback as the single-field version, plus a `MAX_FULL_SCAN_BYTES = 64MB`
cap so a corrupt multi-GB session file can't freeze the picker. Session
reads now open with `O_NOFOLLOW` (falls back to plain RDONLY on Windows
where the constant isn't exposed) — defense in depth against a symlink
planted in `~/.qwen/projects/<proj>/chats/`.

Character handling: `flattenToTail` on the LLM prompt drops a dangling
low surrogate after `slice(-1000)` — otherwise a CJK supplementary char
or emoji cut mid-pair produces invalid UTF-16 that some providers 400.
`sanitizeTitle` applies the same surrogate scrub after max-length trim,
and strips paired CJK brackets (`「」 『』 【】 〈〉 《》`) as whole units so
a `【Draft】 Fix login` doesn't leave a dangling `】` after leading-char
strip. `lineContains` in the title reader is tightened from the loose
substring `'custom_title'` to `'"subtype":"custom_title"'` so user text
containing the literal `custom_title` can't shadow a real record.

Tests: 46 new unit tests across
- `sessionTitle.test.ts` (22): success/all-failure-reasons, tool-call
  filter, tail-slice, surrogate scrub, ANSI/OSC-8 strip, CJK brackets.
- `chatRecordingService.autoTitle.test.ts` (15): trigger/skip matrix,
  in-flight guard, abort propagation on finalize, manual/auto/legacy
  resume symmetry, cross-process race, env opt-out, retry-after-
  transient.
- `sessionStorageUtils.test.ts` (13): single-pass extractor, straddle
  boundary, truncated trailing record, lineContains, multi-field atom.
- `renameCommand.test.ts` (8): `--auto` success, all reasons, sentinel,
  unknown-flag hint, positional rejection, manual/SessionService
  fallbacks.

* docs(session): design doc for auto session titles

Matches the session-recap design doc shape (Overview / Triggers /
Architecture / Prompt Design / History Filtering / Persistence /
Concurrency / Configuration / Observability / Out of Scope) and adds a
Security Hardening section unique to the title path — titles render
directly in the picker and persist in user-readable JSONL, so
LLM-returned control sequences are an attack surface the recap path
doesn't have.

Captures decisions a code-only reader has to reverse-engineer:

- Why `maxAttempts: 1` (best-effort cosmetic metadata; no retry loop).
- Why `autoTitleAttempts` cap is 3 (first turn can be pure tool-call).
- Why the auto trigger does NOT fall back to the main model but
  session-recap does (auto-title fires on every turn; silently charging
  main-model tokens is a bill surprise).
- Why `titleSource: undefined` stays unwritten on legacy records (no
  rewrite risks silently reclassifying user intent).
- Why the cross-process re-read sits between the LLM await and the
  append (manual wins at both in-process and on-disk layers).
- Why `finalize()`'s abort tolerates a controller swap (in-flight
  identity check).
- Why JSON-schema function calling instead of tag extraction (avoid
  reasoning preamble bleed; cross-provider reliability).

Placed at docs/design/session-title/ alongside session-recap,
compact-mode, fork-subagent, and other per-feature design docs. No
sidebar index update required — the design folder is unindexed.

* test(rename): pin model choice in bare /rename kebab path

Addresses reviewer feedback: the bare `/rename` model selection
(`config.getFastModel() ?? config.getModel()`) had no test pinning
it either way. Previous tests mocked `getHistory: []`, which exits
the function before the model is ever chosen, so a silent regression
to either direction (always-main or always-fast) would pass CI.

Two explicit cases now:
- fastModel set → `generateContent` called with `model: 'qwen-turbo'`.
- fastModel unset → `generateContent` called with `model: 'main-model'`.

The tests intentionally mock a non-empty history so the kebab path
reaches the generateContent call site instead of bailing on empty input.
* fix(i18n): sync mismatched keys between en.js and zh.js (QwenLM#3503)

Add 4 keys missing from en.js that are actively used in source code,
add 5 missing Chinese translations to zh.js, integrate check-i18n
into CI to prevent future drift, and skip JSON file write in CI to
avoid dirtying the working tree.

---
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…M#3509)

* fix(cli): remove residual blank lines after MCP init completes (QwenLM#3095)

ConfigInitDisplay rendered <Box marginTop={1}> plus a content line, so
the live area grew by 2 rows during startup. When initialization
finished and the component unmounted, Ink shrank the live area but the
rows it had already committed to the terminal scrollback cannot be
reclaimed, leaving a visible gap above the input.

Move the MCP init status into the Footer's left-bottom status slot
(always mounted, fixed height) so the live area height stays constant
across the init → ready transition. The status participates in the
existing priority chain: ctrlC / ctrlD / escape / vim / shell /
autoAccept / configInit / hint.

* fix(cli): suppress MCP init message when custom status line is active

Audit follow-up. Previously the configInit branch preceded the
suppressHint branch in the footer's left-bottom priority chain. With
a custom status line configured, <Text>{null}</Text> collapses to
zero rows in Ink, so the footer's bottom row went from 1 row during
init to 0 rows after — a 1-row height oscillation that reintroduces
the same scrollback-residue symptom the original fix eliminated in
the default case.

Swap the order so suppressHint short-circuits to null first: the
init message now shares the hint's suppression rule, keeping the
footer's height constant in every configuration.

Also:
- Gate the hook's return on isConfigInitialized directly instead of
  letting the effect clear state, avoiding a one-frame flash where
  the stale "Initializing..." message leaks through on the first
  render after init completes.
- Cover the new behavior with three Footer tests, including a
  regression test for the custom-status-line case.

* fix(cli): show MCP init progress even under a custom status line

Reverting a UX trade-off introduced in the previous commit. That
change suppressed the init message whenever a custom status line was
active, arguing that <Text>{null}</Text> collapses to zero rows in
Ink and any non-zero init row would re-create a one-row shrink on
completion.

Zero shrink was the wrong goal. Hiding init progress from users who
have configured a status line is a real usability loss — the status
line does not surface MCP connection state, so those users now see
no feedback during startup. A one-time, one-line shrink on init
completion is a far smaller regression than the original two-row
scrollback residue this PR was created to fix, and strictly better
than the silent alternative.

Keep the init message in the left-bottom slot and let it sit above
suppressHint in the priority chain. Update the regression test so
that it pins the new behavior (init is visible with or without a
status line) and prevents the suppression from being reintroduced.

* fix(cli): keep MCP init progress visible in screen-reader mode

Footer is gated behind !isScreenReaderEnabled, so moving the init
message inside Footer silenced it for screen-reader users. Render the
same message as a plain Text node in Composer when the screen reader is
active — screen-reader users don't suffer from the live-area residual
row issue that motivated the original move, so an independent node is
safe for them.

* refactor(cli): drop duplicated screen-reader init path and show progress under YOLO

- ScreenReaderAppLayout already mounts <Footer /> directly, so the
  separate <Text> branch in Composer was producing a duplicated
  'Connecting to MCP servers...' line in screen-reader mode. Remove it.
- Move configInitMessage ahead of AutoAcceptIndicator in the footer's
  priority chain so users launched with YOLO / auto-accept-edits still
  see the ~1s startup progress; the approval-mode indicator takes over
  as soon as init finishes.
- Add unit tests for useConfigInitMessage covering the idle, progress,
  reset, and unsubscribe paths.
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
…ased approach (QwenLM#3502)

* feat(web-search): add GLM (ZhipuAI) web search provider

- Add GlmProvider class implementing BaseWebSearchProvider using the
  ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search)
- Support multiple search engines: search_std, search_pro, search_pro_sogou,
  search_pro_quark
- Support optional config: maxResults, searchIntent, searchRecencyFilter,
  contentSize, searchDomainFilter
- Truncate query to 70 characters per API limit
- Register 'glm' in the provider discriminated union (types.ts) and
  createProvider() switch (index.ts)
- Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class
- Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts
- Forward GLM_API_KEY in sandbox environment
- Update provider priority list: Tavily > Google > GLM > DashScope
- Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts
- Update docs/developers/tools/web-search.md with GLM configuration,
  env vars, CLI args, pricing, and corrected DashScope billing info
- Fix stale OAuth/free-tier references in web-search.md

Closes QwenLM#3496

* docs(web-search): fix DashScope note and add GLM server-side limitations

* fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency

- DashScopeProvider.isAvailable() now checks config.apiKey instead of authType
- Remove OAuth credential file reading and resource_url requirement
- Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search
- Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag
- Forward DASHSCOPE_API_KEY into sandbox environment
- Update integration test to detect DASHSCOPE_API_KEY
- Update docs to reflect new API key based configuration

* feat(web-search): remove built-in web search tool

The web_search tool and all related provider implementations are removed.
Web search functionality will be provided via MCP integrations instead,
which is the direction the broader agent ecosystem is moving.

Removed:
- packages/core/src/tools/web-search/ (entire directory)
- packages/cli/src/config/webSearch.ts
- integration-tests/cli/web_search.test.ts
- ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED
- webSearch config in ConfigParams, Config class, settingsSchema
- CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id,
  --glm-api-key, --dashscope-api-key, --web-search-default
- Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys
- web_search from rule-parser, permission-manager, speculation gate,
  microcompact tool set, and builtin-agents tool list

* fix: remove websearch reference

* docs: remove websearch tool

* docs: add break change guide

* fix review
)

Selecting an older entry from input history via the arrow keys and pressing
Enter now moves that entry to the most recent position, so the next Up press
surfaces it first. Previously two bugs combined to keep stale copies in place:
the history-navigation index was not reset on submit, and deduplication only
collapsed consecutive repeats, leaving non-consecutive duplicates intact.
…3525) (QwenLM#3550)

* refactor(core): make OpenAI converter stateless to prevent shared-state races

Follow-up to QwenLM#3525. QwenLM#3516 showed that OpenAIContentConverter's long-lived
per-pipeline state raced between concurrent streams; QwenLM#3525 scoped the
streaming tool-call parser, this removes the remaining shared state.

- OpenAIContentConverter is now a module of stand-alone functions; the
  exported symbol is a namespace object preserved for call-site
  compatibility.
- New RequestContext (in types.ts, alongside PipelineConfig and
  ErrorHandler) carries model, modalities, startTime, and an optional
  per-stream toolCallParser. The pipeline builds one per request and
  threads it through every conversion call.
- errorHandler drops duration/isStreaming; duration is recomputed from
  startTime at error time and troubleshooting text is uniform.
- convertOpenAIChunkToGemini now throws if toolCallParser is missing so
  future misuse surfaces loudly instead of silently constructing a
  one-shot parser per chunk.

* test(core): align timeout expectations
…'error' event (QwenLM#3481)

* fix: strengthen error handling in launchBrowser to prevent unhandled events

* fix: strengthen error handling with ChildProcess type and debugLogger

* fix: use type-only import for ChildProcess
In ACP mode, the Mcp server list sent by the IDE client can include
SSE (type: "sse") and HTTP (type: "http") transports, but the previous
implementation only handled stdio servers via toStdioServer(). Non-stdio
servers were silently skipped (continue), so any SSE/HTTP-configured
MCP server would never be registered.

Changes:
- Add toSseServer() helper: detects type=="sse" servers and maps them
  to MCPServerConfig(url=..., headers=...)
- Add toHttpServer() helper: detects type=="http" servers and maps them
  to MCPServerConfig(httpUrl=..., headers=...)
- Refactor newSessionConfig() loop to handle all three transport types
- Declare mcpCapabilities: { sse: true, http: true } in agentCapabilities
  so IDE clients know this agent supports these transports without needing
  a transparent proxy
- Export the three helper functions for unit testing

Tests:
- Unit tests for toStdioServer / toSseServer / toHttpServer helpers
  (type discrimination, mutual exclusion)
- Integration-style tests for QwenAgent.initialize() mcpCapabilities
- Integration-style tests for newSession() with SSE/HTTP MCP servers,
  verifying MCPServerConfig is constructed with the correct arguments
  (url vs httpUrl, headers passthrough, empty-headers → undefined)

Fixes QwenLM#3472
…#3463)

* fix(cli): run ACP Agent tool calls concurrently (QwenLM#2516)

When the model returns multiple Agent tool calls in a single turn, the
ACP Session previously executed them sequentially in a plain for-loop,
multiplying latency by the number of sub-agents spawned.

Mirror the partition logic in coreToolScheduler.partitionToolCalls:
consecutive Agent calls form a parallel batch (safe because sub-agents
have no shared mutable state); any other tool forms its own sequential
batch so the model's implicit ordering is preserved. Response-part
ordering still matches the original functionCalls order.

Add a focused test that uses controllable deferred executes to prove
both Agent calls start before either resolves, and that the fed-back
functionResponse ordering is stable regardless of resolution order.

* Address PR QwenLM#3463 review: bound concurrency + robust test timing

Two issues raised by the /review bot:

1. The raw Promise.all fan-out bypassed the bounded-concurrency guard
   that coreToolScheduler applies via QWEN_CODE_MAX_TOOL_CONCURRENCY.
   Replaced with an inline runBounded helper that mirrors core's
   runConcurrently (Promise.race on a bounded executing set, default
   cap 10), keeping in-order result collection.

2. The concurrency test used a 10-iteration microtask yield loop before
   asserting both execute() spies had been invoked. That's fragile —
   runTool's pre-execute path (build → getDefaultPermission →
   evaluatePermissionRules → permission branch → PreToolUseHook) has
   more await boundaries than 10 ticks guarantees, and the CI run
   reported call-a still at 0 invocations at the assertion point.

   Reworked the test to wait on an explicit `called` deferred that
   resolves *inside* the execute() mock body. Under sequential
   behaviour only one `called` would ever fire → `Promise.all([called-a,
   called-b])` deadlocks → vitest's per-test timeout surfaces the
   regression. Under the fix both fire before either result resolves.

* fix(acp): degrade gracefully when AgentTool invocation has no eventEmitter

The concurrency test for QwenLM#2516 timed out on CI with "Test timed out in
5000ms" after the `await Promise.all([called-a, called-b])` rewrite in
the previous review-fix commit. The 5000ms wait was the symptom; the
root cause is that neither `execute()` was ever being called.

runTool's AgentTool branch was guarded with `'eventEmitter' in invocation`,
which is a *key-presence* check. The test mock provides
`{ eventEmitter: undefined, ... }` — the key exists (value undefined),
the branch is entered, and `SubAgentTracker.setup` immediately throws
inside `eventEmitter.on(...)`. The try/catch in runTool swallows the
throw and returns an error response, so `invocation.execute()` never
runs, `called[id].resolve()` never fires, and the test deadlocks.

The earlier review commit (4519c5f) interpreted the CI symptom as
"10 microtask yields aren't enough" and rewrote the assertion around a
deferred `Promise.all`. But the old test's `toHaveBeenCalledTimes(1)`
failure with 0 invocations was already the same bug — execute was never
called. The new formulation just converted the visible failure from an
assertion mismatch into a timeout.

Switch the guard to a truthy check against `invocation.eventEmitter`.
Semantics for real AgentTool are unchanged — `agent.ts:392` declares
`readonly eventEmitter: AgentEventEmitter = new AgentEventEmitter()`,
so production always enters the branch. The only new behavior is that
incomplete invocations (or test mocks) skip SubAgentTracker setup
cleanly instead of crashing. `subAgentCleanupFunctions` stays `[]`,
so the cleanup forEach at the success/error paths is a no-op.
…d 9;5u output (QwenLM#3544)

* fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output

When a Kitty-capable terminal (iTerm2, Kitty, WezTerm) is used, the CLI
enables the Kitty keyboard protocol at startup via ESC[>1u. On exit, the
protocol must be disabled with ESC[<u to restore the terminal's default
key encoding. Failing to do so leaves the terminal in Kitty mode: any
subsequent Ctrl+C press is encoded as ESC[99;5u, and since the shell does
not understand this sequence, it echoes the trailing '9;5u' as garbled
text.

Root cause: kittyProtocolDetector registered cleanup handlers for 'exit'
and 'SIGTERM', but omitted SIGINT. A process terminated via SIGINT (e.g.
kill -INT <pid>, a parent process sending SIGINT, or certain process
managers) would exit without disabling the protocol.

Fix:
1. Add process.on('SIGINT', disableProtocol) alongside the existing
   'exit' and 'SIGTERM' handlers in kittyProtocolDetector.ts.
2. Export a new disableKittyProtocol() function for explicit call sites.
3. Call disableKittyProtocol() in the registerCleanup callback in
   gemini.tsx before instance.unmount(), so the disable sequence is
   written while stdout is fully operational regardless of exit path.

Fixes QwenLM#3528

* fix(test): add disableKittyProtocol to kittyProtocolDetector mock
…LM#3523)

* fix(cli): dispatch queued slash commands through the slash path

When the agent was responding and the user queued a message, the drain
path joined all queued messages with `\n\n` and submitted them as one
prompt. Any slash command in that blob (e.g. `/model`) no longer started
with `/`, so it was sent to the model as plain text instead of opening
the command's dialog.

The mid-turn tool-result drain had the same problem: it drained the
entire queue into the tool-result payload, so a slash command queued
during tool execution was injected as context for the model rather than
executed as a command.

Queue draining now splits into segments — consecutive plain-text
messages are still batched into one submission, while slash commands
are submitted alone so their `/` prefix survives. The mid-turn drain
only takes leading plain-text messages and leaves slash commands
queued for the normal idle drain. The idle drain is gated on open
dialogs so a queued `/model` does not cause the following queued
prompt to be sent to the model while the picker is still open, and a
re-entry lock plus a nonce close the race between state commits and
the async dialog-open.

* fix(cli): defer queued slash commands until idle

* fix(cli): drop queued messages on cancel instead of auto-submitting

Cancel's contract is now "abort and redirect" in both cancel paths:
restore the most recent queued segment into the buffer for editing and
drop the rest, so forgotten follow-ups cannot auto-submit once the turn
settles. Previously the non-tool path left queued plain-text segments
in place for the idle drain to fire, and the tool-executing path
cleared only the buffer — both surprised users with belated message
dispatches after they had already cancelled.

* refactor(cli): batch plain prompts in idle drain

Idle drain now runs in two phases: drain all plain-text prompts into one
turn (drainQueue), then pop slash commands one-by-one (popNextSegment).
Mirrors the mid-turn behavior so queue handling is consistent across
mid-turn and idle contexts.

popAllMessages now drains the entire queue joined with \n\n for Ctrl+C
cancel and ESC/Up edit-restore. Drop the unused options parameter from
useMessageQueue and the extractFirstSegment helper.

---------

Co-authored-by: 愚远 <zhenxing.tzx@alibaba-inc.com>
… sharing (QwenLM#3573)

QwenLM#3450 pinned every assistant/thinking segment in a streamed turn to the
same turn-start timestamp so a later user message could not be sorted
between two segments of the previous turn (QwenLM#3273). That fix turned out
to conflict with the tool-call timeline: tool calls carry their own
arrival timestamp, which is strictly greater than the turn-start
timestamp, so after QwenLM#3450 every tool call sorted AFTER both assistant
segments instead of between them — the exact 'tool call jumped to the
end' ordering bug users are now reporting.

The two bugs pull the sort key in opposite directions and cannot both
be satisfied by a single timestamp strategy. Roll QwenLM#3450 back byte-for-
byte on useMessageHandling.ts so the tool-call ordering regression is
fixed immediately; replace the test file with two focused cases that
pin the conflicting invariants so the next fix (likely a monotonic
sequence key shared across messages and tool calls) has a clear
target:

  - tool-call interleave test (passes today): a tool call that arrives
    between two assistant segments must sort strictly between them.
  - QwenLM#3273 regression test (it.fails today): all assistant segments of
    one turn must sort before a user message sent during the turn.
    Flipped to a normal it() once the proper fix lands.

Refs: QwenLM#3273, QwenLM#3450

Co-authored-by: Qwen-Coder <noreply@qwenlm.ai>
…LM#3575)

- Add new skills: bugfix, feat-dev with structured workflows
- Update existing skills: docs-audit-and-refresh, docs-update-from-diff,
  e2e-testing, qwen-code-claw, structured-debugging, terminal-capture
- Update test-engineer agent with clearer constraints and formatting
- Update qc commands: bugfix, code-review, commit, create-issue, create-pr
- Reorganize .gitignore to keep qwen configs near top
- Expand AGENTS.md with development commands, feature/bugfix workflows,
  project directories table, and code review guidelines

Co-authored-by: 愚远 <zhenxing.tzx@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…e sessions (GH#3579) (QwenLM#3590)

* fix(core): preserve reasoning_content during session resume and active sessions (GH#3579)

* chore(core): remove dead thinkingThresholdMinutes config after latch removal (GH#3579)
* feat(vscode-companion): support /export session command

* fix(vscode-ide-companion/webview): prefer ACP session id for export

* feat(vscode-ide-companion): support /export slash command

Add nested /export completion and ACP command availability for the VS Code companion.

Reuse the shared export flow, write to the default path, and show clickable export results in chat.

* fix(export): align slash command messaging

Restore the CLI export description to the existing wording.

Keep the VS Code companion error message consistent with the required /export subcommands.

* fix(webui): support explicit markdown file links

Handle local markdown file links in assistant messages even when automatic file-link detection is disabled.

Normalize encoded paths and line fragments so exported files can be opened from the VS Code webview.

* test(vscode-ide-companion): make export path assertion cross-platform

* fix(vscode-ide-companion): use public session export entrypoint

* fix(cli): replay standalone ESC after early capture

* fix(vscode-ide-companion): resolve rebase artifacts and vitest export alias

Remove duplicate AvailableCommand import caused by merge, and add
vitest resolve alias for @qwen-code/qwen-code/export so the session
export service tests can resolve the CLI export module from source.

* fix(cli): fix getAvailableCommands test mock to use getCommandsForMode

The test mock was only setting up getCommands but getAvailableCommands
calls getCommandsForMode. Add getCommandsForMode to the mock and set up
test data on it instead.

* fix(vscode-ide-companion): fix export file link click and add save dialog

- Fix file:/// URI handling in MarkdownRenderer: normalizeExplicitFileLink
  now strips the file:// scheme before checking isAbsolutePath, so exported
  file links are properly recognized and clickable
- Replace direct cwd file write with vscode.window.showSaveDialog() so
  users can choose the export destination and filename
- Handle cancelled save dialog gracefully (return null, skip success message)

* fix(webui): scope file link handler to file:// URIs only, fix # in filenames

- normalizeExplicitFileLink now returns early for file:// URIs without
  splitting on #, since vscode.Uri.file() encodes # as %23 in the path.
  This prevents filenames containing # from being truncated after decode.
- Explicit-link click handler now only fires for file:// URI hrefs,
  not arbitrary relative paths. This prevents model-generated markdown
  links from bypassing enableFileLinks=false and opening arbitrary files.
- Remove unused KNOWN_FILE_EXTENSIONS constant.

* fix(vscode-ide-companion): update export tests for save dialog, fix stale JSDoc

- Add showSaveDialog mock to sessionExportService.test.ts
- Update existing test to verify save dialog is called with correct args
- Add test for cancelled save dialog returning null
- Fix JSDoc that incorrectly claimed fallback-to-cwd behavior
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Update version from 0.15.1 to 0.15.2 across all packages and lockfile
- Remove reference to non-existent `packages/cli/LSP_DEBUGGING_GUIDE.md`
- Remove reference to unimplemented `/lsp status` slash command
- Replace incorrect `DEBUG=lsp*` env var with actual debug log location
  (`~/.qwen/debug/` session files with `[LSP]` tag)
- Remove external Claude Code documentation links (`code.claude.com`)
- Document `isPathSafe` constraint: absolute paths outside workspace
  are blocked, users must add server binary directory to PATH
- Add practical troubleshooting: `ps aux | grep <server>` to check
  if the server process is actually running
- Add clangd-specific guidance: `--background-index`, `compile_commands.json`
  location, and `--compile-commands-dir` usage
- Simplify trust documentation (remove vague "configure in settings")
Previously, `isPathSafe` rejected any command containing a path
separator that resolved outside the workspace directory. This blocked
legitimate use cases where users specify absolute paths to language
server binaries (e.g. `/usr/bin/clangd`, `/opt/tools/jdtls/bin/jdtls`).

The fix allows:
- Bare command names resolved via PATH (unchanged)
- Absolute paths (explicit user intent, already gated by trust checks)
- Relative paths within the workspace (unchanged)

Only relative paths that traverse outside the workspace (e.g.
`../../malicious-binary`) are still blocked.

Closes: server silently fails to start when users configure absolute
paths in `.lsp.json`, with only a debug log warning visible.
…abled

The model was not using the LSP tool because the system prompt's
"Tool Usage" section never mentioned it. The tool description alone
("ALWAYS use LSP as the PRIMARY tool") was insufficient — models
follow system prompt instructions more reliably than tool descriptions.

Changes:
- getCoreSystemPrompt() accepts `options.lspEnabled` parameter
- When LSP is enabled, injects an instruction in the Tool Usage section
  telling the model to ALWAYS use the LSP tool FIRST for code
  intelligence queries (definitions, references, hover, symbols, etc.)
  instead of falling back to grep/readfile
- Updated client.ts to pass config.isLspEnabled() to the prompt builder
- Updated test mocks and snapshots
The model avoided calling LSP for findReferences, hover, etc. because
these operations required filePath + line + character which the user
rarely provides. The model would read files directly instead.

Changes:
- Add `symbolName` optional parameter to LspTool
- When symbolName is provided without line/character, auto-resolve
  the symbol's position via workspaceSymbol before executing the
  actual operation (findReferences, hover, goToImplementation, etc.)
- Update tool description with examples showing symbolName usage
- Move LSP priority instruction to top of system prompt for visibility
- Add debug logging for LSP prompt injection

This enables natural queries like:
  {operation: "findReferences", symbolName: "Calculator"}
  {operation: "hover", symbolName: "addShape"}
without requiring the user to know exact file positions.
When LSP is enabled, the model often chose grep or readfile instead
of LSP for code intelligence queries. Now the competing tools'
descriptions include a note reminding the model to use the LSP tool
for definitions, references, symbols, hover, diagnostics, etc.

This "push-pull" approach:
- System prompt pushes toward LSP (top-level priority instruction)
- Grep/ReadFile descriptions pull away from code intelligence usage
…supported

The doc still said "absolute paths outside the workspace are not
supported" but the code was changed to allow them. Updated all
three places (Required Fields table, Troubleshooting, Debugging)
to reflect that absolute paths are now accepted.
- Add test for intermediate path traversal (./a/../../../etc/passwd)
- Add test for forward-slash relative paths (tools/clangd)
- Replace Chinese JSDoc with English on requestUserConsent
@BingqingLyu

BingqingLyu commented May 7, 2026

Copy link
Copy Markdown
Owner Author

Conflict Group 1

This PR shares modified functions with 33 other PR(s): #1, #10, #100, #106, #107, #109, #112, #113, #114, #14, #17, #18, #2, #20, #21, #22, #26, #31, #36, #42, #46, #52, #6, #61, #7, #71, #75, #80, #86, #88, #90, #94, #96.

These PRs should be reviewed as a batch — merging one may affect the others.

Function File Also modified by
SessionListItemView SessionPicker.tsx #112, #113, #114, #88
TestContextConsumer AppContainer.test.tsx #113, #114, #88
authWithQwenDeviceFlow qwenOAuth2.ts #114, #88
buildApiHistoryFromConversation sessionService.ts #114
buildOpenAIRequestForLogging loggingContentGenerator.ts #1, #113, #114, #61, #88
buildPermissionRules rule-parser.ts #112, #113, #114, #88
buildRequest pipeline.ts #1, #113, #114, #61, #88
consumePendingMemoryTaskPromises client.ts #114, #75
convertGeminiResponseToOpenAIForLogging loggingContentGenerator.ts #1, #113, #114, #88
convertGeminiToolParametersToOpenAI converter.ts #1, #113, #114, #88
createRequestContext pipeline.ts #1, #113, #114, #88
createStreamWithChunks Session.test.ts #114, #88
createStreamingInputWithControlPoint permission-control.test.ts #112, #113, #114, #71, #88
createToolRegistry config.ts #112, #113, #114, #88
detectAndEnableKittyProtocol kittyProtocolDetector.ts #114, #88
determineProvider index.ts #1, #113, #114, #88, #90
executeStream pipeline.ts #1, #113, #114, #88
extractToolNameFromRecord export-html-from-chatrecord-jsonl.js #112, #113, #114, #88
finalize chatRecordingService.ts #112, #113, #114, #88
findSessionsByTitle sessionService.ts #112, #113, #114, #88
fromAsync client.test.ts #114
generateSessionTitle renameCommand.ts #112, #113, #114, #88
getTimeoutTroubleshootingTips errorHandler.ts #1, #113, #114, #88
getToolCallComponent ChatViewer.tsx #112, #113, #114, #42, #88
handleQwenAuth handler.ts #112, #113, #114, #36, #88
initialize acpAgent.ts #114, #88
isBrowserLaunchSuppressed config.ts #112, #113, #114, #88
isSdkMcpServerConfig config.ts #106, #112, #113, #114, #18, #46, #75, #86, #88
isToolExecuting AppContainer.tsx #100, #107, #109, #113, #114, #52, #88, #96
isValidSessionId config.ts #112, #113, #114, #88
listSessions sessionService.ts #112, #113, #114, #88
loadCliConfig config.ts #106, #112, #113, #114, #36, #46, #75, #86, #88
main esbuild.js #114, #20
main check-i18n.ts #112, #113, #114, #2, #88
makeConfig permission-manager.test.ts #112, #113, #114, #88
makeModelMessage microcompact.test.ts #114
newSessionConfig acpAgent.ts #114, #88
normalizeConfigOutputFormat config.ts #106, #112, #113, #114, #18, #75, #86, #88
parseApprovalModeValue config.ts #10, #112, #113, #114, #21, #22, #36, #46, #86, #88
parseArguments config.ts #10, #112, #113, #114, #14, #17, #18, #21, #22, #31, #36, #46, #7, #86, #88
parseRules rule-parser.ts #112, #113, #114, #88
prompt Session.ts #114, #88
readLastJsonStringFieldSync sessionStorageUtils.ts #112, #113, #114, #88
recordSlashCommand chatRecordingService.ts #112, #113, #114, #88
recordUiTelemetryEvent chatRecordingService.ts #112, #113, #114, #88
refreshAccessToken qwenOAuth2.ts #114
renameSession sessionService.ts #112, #113, #114, #88
resetChat client.ts #114
resolveDefaultPermission permission-manager.ts #112, #113, #114, #88
sendStreamEnd SessionMessageHandler.ts #114
sessionExists sessionService.ts #114
shouldReplayPendingAtStop earlyInputCapture.ts #114
startInteractiveUI gemini.tsx #114, #14, #17, #31, #6, #88, #94
start_sandbox sandbox.ts #10, #112, #113, #114, #26, #7, #88, #94
toPosixPath rule-parser.ts #112, #113, #114, #88
toStdioServer acpAgent.ts #114, #88
useCompletionTrigger useCompletionTrigger.ts #114, #80
useDreamRunning Footer.tsx #112, #113, #114, #86, #88, #96
useMessageQueue useMessageQueue.ts #114
validateToolParamValues read-file.ts #112, #113, #114, #88

Posted by codegraph-ai conflict detection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicting-group-1 Conflicting PR group 1 — review as a batch conflicting-pr Shares at least one cross-PR dependency with other PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.