feat: add /diff command and git diff statistics utility#103
Open
BingqingLyu wants to merge 60 commits into
Open
feat: add /diff command and git diff statistics utility#103BingqingLyu wants to merge 60 commits into
BingqingLyu wants to merge 60 commits into
Conversation
Port numstat + unified-diff parsing into `packages/core/src/utils/gitDiff.ts` to surface structured working-tree change summaries (files changed, lines added/removed, per-file hunks) against HEAD. Caps mirror issue QwenLM#2997: 50 files, 1MB per file, 400 lines per file, with a 500-file short-circuit via `git diff --shortstat` to avoid expensive work on massive diffs. - `fetchGitDiff(cwd)` returns stats + per-file summaries (tracked + untracked). - `fetchGitDiffHunks(cwd)` returns structured hunks on demand. - `resolveGitDir(cwd)` follows `.git` file indirection so linked worktrees and submodules report the correct gitdir. - Transient-state short-circuit covers merge, cherry-pick, revert, and both `rebase-merge` / `rebase-apply` layouts. - `core.quotepath=false` is forced so non-ASCII filenames stay as UTF-8. Refs QwenLM#2997
Surface the `fetchGitDiff` utility through an interactive `/diff` command. Prints a header (`N files changed, +A / -R`) followed by per-file rows with padded add/remove counts. Untracked files are marked `?`, binary files are marked `~`. When the change set exceeds the per-file cap, a trailing `…and N more` note tells the user how many entries are hidden. Returns a `MessageActionReturn` so it renders the same way in interactive and non-interactive modes.
- Wrap `fetchGitDiff` in try/catch so permission errors on `.git` surface as a friendly error message instead of crashing the action. - Declare `supportedModes: ['interactive', 'non_interactive', 'acp']` so the command is reachable outside the interactive Ink UI — the default for `commandType: 'local'` is interactive-only. - Align `?` (untracked) and `~` (binary) markers with the `+X -Y` stat column via a padded prefix, so filenames line up regardless of row kind. - Drop the "…and N more" hint when no rows are shown (shortstat fast-path with >500 files) — the count alone is sufficient and "showing first 0" is noise. - Switch header to full-phrase i18n templates (separate singular/plural variants) instead of word-by-word `t()` calls that don't survive non-English locales. - Extend tests to 12 scenarios: empty cwd, fetch rejection, singular "file" form, mixed untracked/binary/tracked alignment, 4-digit padding, shortstat fast-path, and supportedModes declaration. Mocks carry a `satisfies GitDiffResult` annotation so shape drift in core breaks the test at compile time.
…#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.
- Remove invalid `commandType` field from diffCommand (SlashCommand has no such property; caused a TS build failure). - Drop duplicate `NumstatResult` interface in gitDiff.ts — it is structurally identical to `GitDiffResult`. - Register the 9 missing `/diff` i18n strings in en.js / zh.js so the command is translatable (previously only `Configuration not available.` had entries).
…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
- fetchUntrackedPaths now uses `ls-files -z` so filenames containing newlines, tabs, or non-ASCII bytes round-trip cleanly instead of being C-style quoted and split into phantom entries. - fetchGitDiff runs the `--shortstat` probe and the untracked-paths lookup in parallel, since both are needed regardless of which path the function takes. - parseGitDiff measures per-file diff size via Buffer.byteLength so MAX_DIFF_SIZE_BYTES matches its documented meaning on non-ASCII diffs. - Adds a regression test for an untracked file whose name contains a literal newline.
Addresses the five open review threads on QwenLM#3491: - parseShortstat: anchored and bounded the regex (`^...$` with `\d{1,10}`) so adversarial inputs can no longer drive polynomial backtracking. Closes CodeQL alert QwenLM#137. - fetchGitDiff: only parse the untracked-path list when we actually need it; the fast path now counts NUL bytes in the raw `ls-files -z` stdout (wenshao P1). - fetchGitDiff: base the `MAX_FILES_FOR_DETAILS` short-circuit on `tracked + untracked`, so repos with few edits but many untracked files still take the summary-only path (wenshao P2). - fetchGitDiff: count newlines in each untracked text file (binary sniff + 1 MB read cap) and fold that into both the header `+N` and the per-file row, so a brand-new file no longer renders as `+0 / -0` (BZ-D P2). - parseGitNumstat: switch to `git diff --numstat -z`. The parser now uses index-based slicing and a rename-pair state machine, so tracked filenames containing tabs/newlines/non-ASCII keep their real bytes (BZ-D P3). Renames collapse into a single `old => new` entry. UI: untracked rows render as `+N filename (new)` (or `~ filename (binary, new)`) instead of the placeholder `?` marker; `/diff` now shows real additions for fresh files.
…m display Two issues surfaced during a directionless multi-round audit of the /diff feature: 1. `countUntrackedLines` reads at most `UNTRACKED_READ_CAP_BYTES` (1 MB) per file, so a 10 MB new log was silently reported as `+~20k` when the real count is ~10×. The helper now `fstat`s the file and returns a `truncated: true` flag when size exceeds the read window; `/diff` surfaces it as `(new, partial)` so the `+N` isn't read as exact. 2. Line-count aggregation was coupled to the per-file display cap: when tracked changes filled the `MAX_FILES` slot, untracked line counts beyond the remaining slots were dropped from `stats.linesAdded` entirely (header under-reported additions). Decoupled: we now read up to `MAX_FILES` untracked files for their line counts regardless of display slots, and only restrict the visible rows to `remainingSlots`. Added regression tests for both: a 1.5 MB new file asserts `truncated: true` and a lower-bound line count, and a `MAX_FILES`-saturated tracked set + 5 untracked files asserts that untracked additions still appear in the header totals even though none of them get displayed.
…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>
`diff --git a/X b/Y` is ambiguous when X contains ` b/` — a file literally named `a b/c.txt` produces `diff --git a/a b/c.txt b/a b/c.txt` with no escape or quoting, and the previous regex `^a\/(.+?) b\/(.+)$` keyed the hunks under the wrong path. Consumers of the exported `fetchGitDiffHunks` API would then fail to correlate hunks with stats or editor paths. Introduces `extractFilePath(lines)` which walks the block for the unambiguous markers (`rename to` / `copy to` / `+++ b/<path>` with a `/dev/null` fallback to `--- a/<path>`) and strips the trailing TAB git appends to paths containing whitespace. Adds unit tests for the `a b/c.txt`, rename, delete, and new-file cases plus an end-to-end test that creates a real `a b/c.txt` file and asserts `fetchGitDiffHunks` keys the hunks correctly. Addresses wenshao review comment #3136657141 on QwenLM#3491.
…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>
The /diff stats used to come back as a plain-text MessageActionReturn. Pipes and ACP still get that, but in interactive terminals we now dispatch a structured history item so the numbers can carry theme colors. - packages/cli/src/ui/types.ts — new DiffRenderRow / DiffRenderModel / HistoryItemDiffStats, MessageType.DIFF_STATS. - packages/cli/src/ui/components/messages/DiffStatsDisplay.tsx — renders +N in theme.status.success (green), -M in theme.status.error (red), and the (new) / (binary) / (new, partial) markers in theme.text.secondary (dim). Column alignment matches the plain-text fallback. - packages/cli/src/ui/components/HistoryItemDisplay.tsx — routes the new item type. - packages/cli/src/ui/commands/diffCommand.ts — builds a DiffRenderModel once and fans out: interactive calls context.ui.addItem; other modes fall through to renderDiffModelText() for the plain-text path. Error and "clean tree" branches keep the existing info/error MessageActionReturn in every mode. - Tests: existing diffCommand suite moved to an explicit non_interactive context (it was asserting text content); new interactive suite covers addItem dispatch and model shape; DiffStatsDisplay component tests cover the four row variants and the "…and N more" note.
…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
…wenLM#3477) * feat(vscode): add native context menu copy actions for webview chat Add three right-click context menu items to the chat message area using VSCode's native webview/context API: - Copy Message: copies the right-clicked message's raw markdown content - Copy All Messages: copies the full conversation in markdown format - Copy Last Reply: copies the last assistant response Implementation details: - Commands registered in package.json with webview/context menu entries - Clipboard writes go through extension host (vscode.env.clipboard) for reliability in webview sandbox - Message identification via data-msg-idx stamped after render - Tool-call outputs supported including diff format (git diff style) - i18n support via package.nls.json (English) and package.nls.zh-cn.json - Menu only shown in message area (not input box or empty state) Closes QwenLM#3052 * fix(vscode): wrap tool-call content text in code blocks for copy * fix(vscode): only wrap tool-call content in code blocks for Copy All, not single Copy Message * fix(vscode): route copy commands to the right-clicked webview and use dynamic code fences * fix(vscode): use childIndexMap for copy-message routing and extract shared message handling Replace the wrapper-div approach (which broke CSS layout) with a render-time childIndexMap that maps DOM child positions to allMessages indices. This avoids both the useLayoutEffect index-drift bug and the wrapper-div CSS side effects. - Remove data-msg-idx wrapper divs; messages render directly as container children, preserving original [&>*] CSS layout - Build childIndexMap during MessageList render, skipping null items (empty AssistantMessage, hidden tool calls via shouldShowToolCall) - findMessageIndex walks up from click target to container's direct child, then maps through childIndexMap - Filter hidden tool calls and empty content in copyAllMessages - Extract handleCommonWebviewMessage to deduplicate routing logic across sidebar, editor panel, and restored panel handlers - Clear lastContextMenuProvider on dispose to prevent memory leaks * fix(vscode): handle image messages in copy and resolve intermittent copy failure - Copy Message on image messages now outputs markdown format  instead of empty string - Copy All Messages includes image messages as  instead of skipping them - Copy Last Reply skips empty assistant placeholders during streaming - Resolve intermittent copy failure by pre-resolving message index on right-click instead of storing a DOM element reference that can become stale after React re-renders
* perf(core): make chat recording writes async
Every recorded chat event (user message, assistant turn, tool call,
tool result, slash command, etc.) was issuing 4 sync fs syscalls on
the main event loop: existsSync(dir) + mkdirSync(dir) + existsSync(file)
+ appendFileSync(file). For a tool-heavy prompt this added ~88 sync
I/O calls per session, blocking the UI render and keypress handler
during each one.
- chatRecordingService.appendRecord: cache ensure-flags so dir/file
creation runs once per session, then enqueue the actual write on a
per-instance promise chain (writeChain). lastRecordUuid is updated
synchronously so chained createBaseRecord still sees the right
parentUuid without waiting for the previous write.
- chatRecordingService.flush: drains the chain — wired into
Config.shutdown so no records are lost on exit.
- jsonl-utils.writeLine: now actually async (fs.promises.mkdir +
fs.promises.appendFile) with per-dir mkdir cache. The existing
per-file mutex still serializes writes correctly.
- Tests updated to await flush() before assertions.
Trace measurement on a single tool-heavy prompt: 110 → 20 sync I/O
calls (-82%), with chatRecordingService dropping from 88 to 0.
* perf(core): cache repeated fs lookups on tool hot path
Each tool invocation went through validatePath → isPathWithinWorkspace
→ fullyResolvedPath, plus its own existence/dir checks. The same paths
got re-resolved across back-to-back tool calls, and ripGrep re-
discovered .qwenignore on every Grep.
- workspaceContext.fullyResolvedPath: bounded LRU on input path
(1024, FIFO). Failed resolutions are NOT cached so retries work.
- paths.validatePath: cache positive isDirectory results; ENOENT
falls through every time so a freshly created file is picked up
immediately.
- ripGrep: module-level caches for searchPath-is-dir and per-dir
.qwenignore presence (256 each, FIFO).
- fileUtils.processSingleFileContent: drop the existsSync gate;
let fs.promises.stat throw ENOENT and convert to FILE_NOT_FOUND
in catch.
Trace: 20 → 10 sync I/O calls. Cumulative reduction since the
chat-recording change: 110 → 10, -91%. All 6057 core tests pass.
* test(core): cache reset hooks + regression-guards from audit
Self-review pass on the previous two perf commits surfaced a few
follow-ups worth pinning down before they bite:
- Module-level caches (paths.isDirectoryCache, ripGrep dirIsDir/qwen-
Ignore, jsonl-utils.ensuredDirs) persisted across vitest cases
silently. Added underscore-prefixed `_reset*ForTest` exports and
wired one into the validatePath describe block so future cases
mutating the same absolute paths can't pass by accident.
- Documented the parentUuid-chain tradeoff on chatRecordingService
.appendRecord: when the async write rejects, lastRecordUuid was
already set sync, so subsequent records reference an absent
ancestor — readers like sessionService.reconstructHistory then
silently drop those descendants. Same observable failure mode as
the prior sync code's caught-and-logged throw.
- Documented the dir<->file mutation and mid-session .qwenignore
staleness windows for the validatePath / ripGrep caches.
- Added regression tests:
* validatePath does NOT cache ENOENT (Edit-then-Read works)
* validatePath skips re-stat on cache hit (perf assertion)
* flush() resolves immediately on a fresh service
* a rejected writeLine does not block the next record
Full core suite: 6061 pass, 2 skipped — no regressions.
* fix(core): cache chatsDirEnsured only on mkdir success
Pre-fix, the flag flipped to true even when mkdirSync threw, so a single
transient failure (NFS EACCES, sandbox mount race, parent dir briefly
missing) would short-circuit every subsequent appendRecord and silently
drop the rest of the session's transcript with no error surfaced.
Reported by zhangxy-zju on QwenLM#3581.
* fix(cli): destroy stdout instead of process.exit on EPIPE
Routine CLI patterns like `qwen -p ... | head -1` / `| less` / `| grep -m1`
close the downstream pipe and trigger EPIPE. The previous handler called
process.exit(0), which bypassed the caller's runExitCleanup -> Config
.shutdown -> chat-recording flush() chain and silently dropped queued
JSONL writes (most recent assistant turn + tool results).
Destroying stdout instead lets writes fail fast and the natural function
return drive cleanup. We deliberately do not also abortController.abort()
here: the abort path runs handleCancellationError which itself calls
process.exit(130), re-introducing the same bypass.
Reported by zhangxy-zju on QwenLM#3581.
* fix(cli): bound runExitCleanup with per-fn + wall-clock timeouts
Pre-fix, runExitCleanup was an unbounded series of awaits. After the
async-jsonl change moved chat-recording writes off the calling thread
(Config.shutdown now `await flush()`s the queue), any hung syscall
(slow disk, dead NFS mount, stuck MCP socket, telemetry HTTP stall)
would hang process exit indefinitely — sync writes were inherently
bounded by syscall return; async writes are not.
Adds per-cleanup 2s + overall 5s wall-clock failsafes on the same
shape as Claude Code's gracefulShutdown.ts. Also replaces dead
test-isolation code (`global['cleanupFunctions']` was never on global,
the array is module-private) with a `_resetCleanupFunctionsForTest`
hook matching the convention from d648596.
Follow-up flagged by zhangxy-zju on QwenLM#3581.
---------
Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
…y is absent (QwenLM#3495) * fix(core): preserve settings-sourced apiKey when registry model envKey is absent (QwenLM#3417) On restart, `applyResolvedModelDefaults` unconditionally cleared the apiKey resolved from `settings.security.auth.apiKey` (layer 4 fallback) and only read from `process.env[model.envKey]`. When the provider-specific env var was absent (e.g. key stored only in settings), the correctly resolved key was discarded, causing a 401 error. Now capture the previously-resolved apiKey before clearing and fall back to it when `process.env[model.envKey]` is empty, but only for safe source kinds (`settings` and general `env` without `via.modelProviders`). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): also preserve CLI-sourced apiKey during syncAfterAuthRefresh Address review feedback: keys passed via CLI flags (e.g. --openaiApiKey) were dropped on restart because source kind 'cli' was not in the fallback allowlist. Add 'cli' to the condition and a regression test. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): move apiKey preservation from applyResolvedModelDefaults to syncAfterAuthRefresh The previous fallback logic inside applyResolvedModelDefaults could leak a settings/cli-sourced apiKey to a different provider when switching models within the same authType (e.g. dashscope → openai). This is a credential safety issue because the two providers may have different baseUrls. Move the save/restore logic to syncAfterAuthRefresh Step 1, guarded by an `isUnchanged` check (same authType AND same modelId). This ensures: - Restart scenario: apiKey preserved (same model, no change) - Cross-provider switch: apiKey cleared (different modelId) Also adds two cross-provider switch tests (settings-sourced and CLI-sourced) per review feedback. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): replace non-null assertion with truthiness guard and add cold-start test - Replace `savedApiKeySource!` with a truthiness guard for safer source restoration - Add test for cold-start scenario (previousAuthType undefined) to verify no key preservation occurs on first syncAfterAuthRefresh - Fix stale "short-circuit" comment in programmatic key test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): detect provider config hot-reload in isUnchanged check When a model provider config is hot-reloaded (e.g. via Coding Plan update) changing envKey or baseUrl while keeping the same model id, the save/restore logic must not preserve the old apiKey. Extend the isUnchanged guard to compare apiKeyEnvKey and baseUrl against the resolved model, but only after applyResolvedModelDefaults has run at least once (apiKeyEnvKey !== undefined). On first startup call these fields are still unset, so the check is skipped to preserve the settings/cli-sourced key correctly. Adds two hot-reload tests (envKey change and baseUrl change). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): use baseUrl source as hasBeenApplied signal for provider change detection Replace `apiKeyEnvKey !== undefined` guard with `baseUrl source === 'modelProviders'` to reliably detect whether applyResolvedModelDefaults has been called before. This fixes two edge cases: 1. No-envKey models: hot-reload changing baseUrl was undetected because apiKeyEnvKey remained undefined. Now baseUrl source is checked. 2. Startup with envKey but omitted baseUrl: undefined !== default URL could falsely trigger isProviderChanged. Now skipped at startup since baseUrl source is not yet 'modelProviders'. Updates hot-reload test fixtures to simulate post-apply state (baseUrl source as 'modelProviders') and adds no-envKey hot-reload test. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(core): shallow-clone savedApiKeySource to avoid mutation risk Copy the ConfigSource object before applyResolvedModelDefaults runs, so a future refactor that mutates source objects in place won't break the save/restore logic. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* docs(telemetry): clarify Alibaba Cloud console entry Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(telemetry): fix unreachable intl console URL and split new/legacy console guidance - Replace unreachable tracing-sgnew.console.alibabacloud.com with the verified arms.console.alibabacloud.com for international users - Separate OTLP endpoint retrieval steps by console version: new console uses Integration Center, legacy console uses Cluster Configurations → Access point information 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * docs(telemetry): align target example with current implementation Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(telemetry): clarify Alibaba Cloud OTLP setup Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs(telemetry): remove stale TOC entry Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(cli): add sticky todo panel to app layouts * fix(cli): hide sticky todos during feedback dialog
…ar reference crash (QwenLM#3630) When --telemetry-outfile is configured, FileSpanExporter.serialize called JSON.stringify directly on OTel ReadableSpan instances. The spans hold a back-reference to BatchSpanProcessor (._shutdownOnce -> BindOnceFuture._that -> BatchSpanProcessor), which forms a cycle and triggers "TypeError: Converting circular structure to JSON" on every export. Combined with DiagConsoleLogger, the error was repeatedly printed to stderr and polluted the Ink TUI. Switch FileExporter.serialize to the existing safeJsonStringify utility, matching the upstream gemini-cli fix so future merges stay clean. Add a focused regression test that mimics the BatchSpanProcessor cycle shape; broader cycle behavior is already covered by safeJsonStringify.test.ts. Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
…nLM#3613) (QwenLM#3620) Some OpenAI-compatible servers (notably sglang's deepseek-v4 jinja template) crash on the array form of message content even when it carries a single text block, with `TypeError: sequence item 0: expected str instance, list found` at `encoding_dsv4.py:336`. The DeepSeekOpenAICompatibleProvider already flattens content arrays into joined strings in buildRequest, but isDeepSeekProvider only matched on the official api.deepseek.com baseUrl. DeepSeek models served behind sglang / vllm / ollama / etc. bypass the workaround and hit the bug. Extend the matcher to also detect by model name (case-insensitive substring 'deepseek'), so any OpenAI-compatible endpoint serving a DeepSeek model picks up the same content-format flattening. Fixes QwenLM#3613 Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
…tion (QwenLM#3567)" (QwenLM#3633) This reverts commit 007a109. The change made `OPENAI_MODEL` outrank `settings.model.name` when looking up the active entry in `settings.modelProviders`. Combined with the core resolver's `modelProvider > cli > env > settings` priority, this caused a regression: a `/model` selection (which writes `settings.model.name`) was silently overridden whenever `OPENAI_MODEL` was set in the user's shell, with no warning surfaced. Restoring the previous behavior — looking up the provider entry by `argv.model || settings.model?.name` — preserves the implicit contract that an explicit `modelProviders` config takes precedence over stale shell defaults. Users without a `modelProviders` config are unaffected: env vars still drive model selection through the core resolver. See discussion on QwenLM#3567.
* Initial version * Some fixes * Fix sentences * More fixes * Fix * Latest fixes
…message submit (QwenLM#3609) * fix(vscode-companion): slash command completion not triggering after message submit After submitting a message, the input field is cleared with a zero-width space (\u200B) to maintain contentEditable height. When the user then types "/", the DOM content becomes "\u200B/" and the trigger character lands at position 1 instead of 0. The word boundary check only recognized regular space and newline, so the zero-width space was rejected as an invalid boundary — preventing the completion popup from appearing. Add \u200B to the valid word boundary characters so "/" and "@" triggers work correctly after message submission without requiring an extra backspace. Closes QwenLM#3592 * refactor(webui): extract zero-width space placeholder into shared constant Replace scattered `\u200B` magic strings with a shared `ZERO_WIDTH_SPACE` constant and `stripZeroWidthSpaces()` helper exported from @qwen-code/webui. This also improves the slash command completion fix: instead of adding \u200B to the word boundary check, strip it at the source in handleInput (consistent with InputForm's onInput handler) and clamp the cursor position to the stripped text length. Closes QwenLM#3592 * test: add tests for zero-width space handling and shouldSendMessage - Add unit tests for ZERO_WIDTH_SPACE constant and stripZeroWidthSpaces helper (via @qwen-code/webui import) - Add shouldSendMessage tests covering empty, whitespace, zero-width space, and attachment scenarios - Add parseExportSlashCommand tests for zero-width space input * fix(test): use correct ImageAttachment type in shouldSendMessage tests Fix CI lint failure by providing all required ImageAttachment fields (id, name, type, size, data, timestamp) instead of non-existent mediaType property.
…uth paths (QwenLM#3629) * feat(config): support API timeout env override Adds support for QWEN_CODE_API_TIMEOUT_MS as an environment override for model generation timeout. Qwen Code already supports timeout configuration via: settings.model.generationConfig.timeout This change introduces an env-based override for users running slow local/OpenAI-compatible backends where editing config is less convenient. Precedence: modelProvider > env var > settings > default (120000ms) Behavior: - Valid positive env values override configured timeout - Invalid values are ignored - Default behavior remains unchanged (applied in buildClient()) Note: The 5-minute timeout reported in QwenLM#1045 originally came from undici's default bodyTimeout, which is now disabled (bodyTimeout:0). The modelConfigResolver default is 120000ms (2 minutes). Includes unit tests covering precedence and validation. Closes QwenLM#1045 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(core): add edge-case tests for QWEN_CODE_API_TIMEOUT_MS Covers: large timeout values, whitespace-padded env values, negative env values, and reinforces provider > env > settings precedence. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(config): support QWEN_CODE_API_TIMEOUT_MS override Adds support for QWEN_CODE_API_TIMEOUT_MS as an environment override for model generation timeout. Closes #13 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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 QwenLM#3223
Adds argument-hint support across the slash command pipeline. Skill and command authors specify an argument-hint field in markdown frontmatter, which renders as inline ghost text when the user has typed the command name but not yet provided arguments. Pipeline: - Skill parsing: SkillConfig.argumentHint parsed from SKILL.md frontmatter - Command loaders: propagated through SkillCommandLoader, BundledSkillLoader, FileCommandLoader, command-factory - UI: useCommandCompletion shows hint as ghost text with showCursorBeforeText layout; InputPrompt separates display text from Tab-accept text - ACP: passed as input.hint per spec - Bundled skills (batch, loop, qc-helper, review) get hints Hint is excluded from completion menu labels to keep the dropdown clean and disappears as soon as the user starts typing arguments.
…wenLM#3653) Extract duplicated timeout env override block into a shared helper applyTimeoutEnvOverride(), used by both resolveModelConfig() and resolveQwenOAuthConfig(). Preserves precedence: modelProvider > env > settings > default. Adds [Regression] and [Additional] tests guarding against the original OAuth-path bug and covering edge cases.
# Conflicts: # scripts/unused-keys-only-in-locales.json
Audit of the colorize commit found one real DRY hazard: DiffStatsDisplay and renderDiffModelText each independently re-derived addWidth / remWidth / statColumnWidth from the same row list. If anyone later changed one formula, the interactive Ink output and the non-interactive plain text would silently fall out of column alignment. Extract the computation into computeDiffColumnWidths() exported from diffCommand.ts; both renderers now call it. Adds a focused unit test of the contract (empty rows, widest non-binary row wins, binary rows are ignored, untracked text rows count). Drop a redundant `Omit<HistoryItemDiffStats, 'id'>` annotation since the type already has no id field.
Two Critical findings on PR QwenLM#3491: 1. (line 63) When /diff is invoked from a subdirectory of the worktree, `git diff` emits repo-root-relative paths but `git ls-files --others` is scoped to cwd and emits cwd-relative paths. Result: mixed path bases in `perFileStats` and silent omission of untracked files in sibling directories. Resolve `findGitRoot(cwd)` once and run every git invocation (and `path.join(...)` for line counting) from there, so all keys are repo-root-relative and the listing is repo-wide. 2. (line 455) `countUntrackedLines` opened every untracked path with `open(absPath, 'r')`. Git's `ls-files --others` can list FIFOs (whose `open()` blocks indefinitely waiting on a writer) and symlinks (which `open()` dereferences, potentially reading outside the worktree). Add an `lstat` gate: only regular files are counted; symlinks and other special files render as binary `~` rows. Two new integration tests cover both regressions: one creates a sibling untracked file at the repo root and invokes fetchGitDiff from a subdir asserting all three changes (root + sub) come back keyed by repo-root-relative paths; the other creates a symlink pointing at content outside the worktree and asserts it lands as a binary row with no contribution to linesAdded.
The previous fix(core) commit accidentally bundled two unrelated working-tree edits (a test comment in .npmrc and a TODO in README.md) that I had used while sanity-testing /diff. They have nothing to do with the fix; restore them to their pre-bb0164d99 state.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Implements the structured git-diff statistics feature requested in QwenLM#2997 and exposes it through a new
/diffslash command.packages/core/src/utils/gitDiff.tsparsesgit diff --numstat,--shortstat, and unifiedgit diff HEADoutput into a structured result (files changed, lines added/removed, per-file summaries, hunks) with the caps called out in the issue: 50 files, 1 MB per file, 400 lines per file, plus a 500-file short-circuit via--shortstat./diffbuilt-in slash command renders the result as a single info message — headerN files changed, +A / -Rfollowed by per-file rows, with?for untracked files and~for binary files.Screenshots / Video Demo
Typical output inside a dirty working tree:
On a clean tree or outside a git repo,
/diffprints a single info line and exits without error.Dive Deeper
Borrowed the parsing strategy from the reference implementation in claude-code-cli's
utils/gitDiff.ts, then adapted to qwen-code conventions and fixed a handful of issues surfaced during adversarial review:findGitRootreturns the directory that contains.git, but in a linked worktree.gitis a file pointing at<main>/.git/worktrees/<name>/. A newresolveGitDirhelper follows that indirection soMERGE_HEAD,CHERRY_PICK_HEAD, etc. are looked up in the right gitdir.REBASE_HEADalone misses the common case. The transient-state check now looks atrebase-merge/andrebase-apply/directories too.---/+++/indexas file content. The unified-diff parser previously skipped any line starting with those prefixes as file-header metadata, which corrupted hunks that removed or added source lines beginning with---,+++, orindex. Metadata is now only skipped before the first@@hunk header.-c core.quotepath=falseso Japanese / Chinese / other non-ASCII filenames stay as UTF-8 instead of being octal-escaped into keys like\346\227\245\346\234\254\350\252\236.txt.stats.filesCount, so callers see a consistent surface.continuetobreakso we stop scanning once the 400-line cap is reached rather than walking the remainder of a huge file looking for hunk headers./diffrobustness. Wrapped in try/catch (FS permission errors surface as a friendly message rather than crashing the action), declaredsupportedModes: ['interactive', 'non_interactive', 'acp']so the command works outside the Ink UI, and aligned?/~marker rows with the+X -Ystat column so mixed output stays tabular.Reviewer Test Plan
/diff. Expected: a header with files-changed / +added / -removed plus per-file rows./diffin a clean tree, a fresh repo with no HEAD, and a non-git directory — each should print a single info message without throwing.git worktree add ../wt -b feature, drop a fakeMERGE_HEADinside<main>/.git/worktrees/wt/, and confirm/diffshort-circuits with the "merge/rebase/cherry-pick/revert" message — this exercises theresolveGitDirfix.Testing Matrix
Validated on macOS via `npm run typecheck` (0 errors) and the project's vitest runner (1602 core tests pass, 271 CLI command tests pass).
Linked issues / bugs
Resolves QwenLM#2997