Commit graph

5536 commits

Author SHA1 Message Date
github-actions[bot]
d33bb410e5 chore(release): v0.15.0-preview.1 2026-04-21 06:35:51 +00:00
tanzhenxin
8ae1efbf80
test(integration): switch settings-migration probe from --help to mcp list (#3486)
* test(integration): switch settings-migration probe from --help to mcp list

--help is a purely informational command and intentionally does not
load settings. The settings-migration integration test was leaning on
a legacy side effect where --help happened to run loadSettings() during
startup, which in turn persisted the migrated file back to disk. After
the bare startup mode refactor reordered startup so that argument
parsing runs before settings loading, yargs now exits inside parse()
on --help before loadSettings() is ever called, and the test fixtures
stayed at their original version on disk.

Switch the probe to `mcp list`, which is a first-class subcommand that
goes through loadSettings() (and therefore the migration chain and
the write-back) and then exits without needing API credentials or
network. On a fresh test rig with no configured servers it prints a
single line and returns, so the test stays fast.

No production code changes; --help remains side-effect-free.

* test(cli): remove flaky right-arrow prompt suggestion test

The test intermittently fails in CI because the render and stdin write
race with the component's readiness window; covered by the other prompt
suggestion tests in the same file.
2026-04-21 14:19:44 +08:00
tanzhenxin
b27cb81bb7
feat(cli): attribute /stats rows to the originating subagent (#3229)
* feat(cli): attribute /stats rows to the originating subagent

Thread subagent identity through telemetry via an AsyncLocalStorage
context so each API response knows which subagent (or main) emitted
it. Aggregate a per-source breakdown alongside the existing per-model
totals and render one row per (model, source) in /stats and /stats
model. Main-only sessions collapse to the existing single-row display.

Resolves #3215

* fix(cli): reserve `main` subagent name and stabilize /stats React keys

Two latent correctness issues found during self-review of PR #3229:

- A subagent named `main` would silently collide with the `MAIN_SOURCE`
  sentinel and be merged into the main bucket with no attribution. Add
  `main` to the reserved-names list so validation rejects it.
- `flattenModelsBySource` used the normalized display label (with `-001`
  stripped) as the React key, which could collapse distinct models
  `foo` and `foo-001` into duplicate keys. Split `ModelSourceEntry` into
  `{ key, label, metrics }` with `key` built from the raw model name
  (plus `::source` in the split case), and update both `StatsDisplay`
  and `ModelStatsDisplay` to key rows/columns off it.

Also surface invalid-subagent-file parse errors through the debug
logger instead of swallowing them entirely, so users running with
debug logging enabled can tell why a subagent failed to load.

Add a dedicated unit test file for `flattenModelsBySource` covering
the collapse rule, session-wide split, source order, the
`foo`/`foo-001` key-collision regression, and the empty-bySource
fallback. Extend the reserved-name test to include `main`.
2026-04-21 11:44:10 +08:00
Shaojin Wen
52c7a3d0ed
fix(cli): pin /recap above input and align defaults with fastModel (#3478)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(cli): pin /recap above input box and align defaults with fastModel

The recap rendered as a regular history item, so as soon as the model
streamed a new reply the "where you left off" reminder scrolled out of
view. Move it to a sticky banner anchored just above the Composer
(matching how btwItem is rendered) so it stays visible across turns.

While reworking the surface, also:
- Replace the chevron prefix with `※ recap:` so it reads as a labeled
  recap line instead of a generic dim message.
- Mirror the placement in ScreenReaderAppLayout so screen-reader users
  see it in the same logical position.
- Drop HistoryItemAwayRecap from the HistoryItemWithoutId union — it
  is no longer addItem-able, and leaving it in invited silent no-op
  bugs where addItem(awayRecap) would compile but render nothing.
- Clear the banner on /clear, /reset, /new and on /resume into a
  different session, so a recap from a previous context doesn't bleed
  into a freshly started one.
- Re-measure the controls box when the banner appears or disappears
  (its height changes by a couple of lines) so the main content area
  recomputes availableTerminalHeight and stays laid out correctly.

Auto-trigger now defaults to "on iff fastModel is configured" rather
than unconditionally on. Running an ambient background recap on the
main coding model is too costly and slow to be a sane default; tying
it to fastModel means the feature is silently opt-in for users who
have set up a cheap fast model. An explicit `general.showSessionRecap`
override still wins either way, and `/recap` itself is unaffected.

Sharpen the slash-command description to match the new behavior.

* fix(core): silence AbortSignal listener-leak warning in OpenAI pipeline

Every chat.completions.create call wires up an abort listener on the
incoming AbortSignal, and several layers — retryWithBackoff, the
LoggingContentGenerator wrapper, the SDK's own internal stream/fetch
plumbing — register their own listeners against the same signal. Five
retry attempts plus those layers comfortably exceed Node's default
10-listener cap and produce a MaxListenersExceededWarning. With
features that share or compose signals (e.g., recap + followup
speculation firing on the same response cycle), even a higher cap
gets blown past.

The signals here are per-request and short-lived, so the accumulation
is structural rather than a real memory leak — they get GC'd as soon
as the request settles. setMaxListeners(0, signal) at the SDK boundary
disables the warning for these specific signals only, without masking
any genuine leak elsewhere in the process. Idempotent and confined to
the one place where retry-bound API calls cross into the SDK.

* fix(core): tighten recap to a single sentence within 80 chars

The 1-3 sentence budget reliably wrapped onto two lines in the sticky
banner above the input box, which made it visually heavy for what is
supposed to be a glanceable reminder. Constrain the prompt to exactly
one sentence with a hard 80-char cap, and merge the "high-level task
+ next step" rule into a single sentence instead of two adjacent ones.

Also sweep the docs (settings, commands, design) so the user-facing
copy and the internal design notes match the new format.

* fix(cli): apply review feedback for recap PR

Two issues from review:

- The schema description for `general.showSessionRecap` still said
  "1-3 sentence summary" while the prompt, docs, and slash-command
  copy already say "one-line". Aligns the text in settingsSchema.ts
  and the regenerated VSCode JSON schema.

- The /resume wrapper cleared the sticky recap synchronously, before
  the inner handler had a chance to discover that no session data
  was available. On a no-op resume the user would still lose the
  current recap. Make `useResumeCommand.handleResume` return
  Promise<boolean> reporting whether a session actually loaded, and
  only clear the recap on a confirmed switch.

* fix(cli): default showSessionRecap to false and drop fastModel heuristic

The earlier "enabled iff fastModel is configured" default made it hard
for users to answer the simple question "is auto-recap on for me right
now?" — the answer depended on a setting from a different category,
and setting/unsetting fastModel silently changed recap behavior.

Revert to a plain boolean with a conservative off-by-default:

- Auto-trigger fires only when the user explicitly sets
  `general.showSessionRecap: true`.
- Manual `/recap` keeps working regardless (that's a user-initiated
  call, not an ambient one).
- Users never get ambient LLM calls billed to their main coding model
  without having opted in.

Aligns settings.md, design doc, and the regenerated JSON schema.
2026-04-20 23:58:19 +08:00
zhangxy-zju
4d1d430390
feat(cli): make ACP message rewrite timeout configurable (#3475)
* feat(cli): make ACP message rewrite timeout configurable

The rewrite LLM call timeout was hardcoded to 30s. For business
scenarios where the final turn contains a large KPI table or
report body, that call can exceed 30s and get aborted silently —
losing the user-visible conclusion.

Adds optional `timeoutMs` to `MessageRewriteConfig` (default 30000)
so large/slow rewrites can be tuned per deployment.

Fixes #3474

* docs(cli): translate timeoutMs note to English, fix code block
2026-04-20 20:58:58 +08:00
jinye
bf561fa495
fix(core): prevent malformed permission rules from becoming tool-wide catch-alls (#3467)
* fix(core): prevent malformed permission rules from becoming tool-wide catch-alls

A permission rule with unbalanced parentheses (e.g. `Bash(rm -rf /)*`)
was silently parsed with `specifier: undefined`, causing `matchesRule`
to treat it as a catch-all that matches every invocation of the tool.
For deny rules this blocked all commands; for allow rules a typo could
silently auto-approve everything.

Add an `invalid` flag to `PermissionRule`. `parseRule` now marks rules
with unbalanced parens as invalid, `matchesRule` short-circuits them to
never match, and all entry points (`addSession*Rule`, `addPersistentRule`,
`parseRules`) warn on malformed input. `listRules` filters out invalid
rules so they don't appear in the /permissions UI.

* fix(cli): show error in /permissions dialog when adding malformed rule

When a user enters a rule with unbalanced parentheses via the "Add Rule"
input in the /permissions dialog, show an inline error message instead of
silently accepting and then hiding the invalid rule.

Closes #3459

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-20 18:56:14 +08:00
Shaojin Wen
c74d7678cb
Revert "feat(core): add dynamic swarm worker tool (#3433)" (#3468)
This reverts commit f7ebc372f1.
2026-04-20 16:40:14 +08:00
Shaojin Wen
5fedf10419
feat(cli): add tool execution progress messages (#3155)
* feat(cli): add tool execution progress messages with per-tool elapsed time, shell stats, and terminal progress bar

- Show per-tool elapsed time (Ns) next to spinner after 3 seconds of execution,
  covering all tools (not just shell), by piping existing core startTime through
  to the UI layer via IndividualToolCallDisplay.executionStartTime
- Add shell output statistics bar below ANSI output showing +N lines overflow
  count, byte size, and explicit timeout when set by user
- Add terminal tab progress bar via OSC 9;4 sequences for iTerm2, Ghostty, and
  ConEmu, with tmux/screen DCS passthrough support
- Extend AnsiOutputDisplay with optional totalLines/totalBytes/timeoutMs fields
- Add ShellStatsBar component for rendering shell output statistics

* fix(cli): address review feedback — use formatDuration for timeout, pass displayHeight to ShellStatsBar

- Use existing formatDuration() from formatters.ts instead of inline
  timeout formatting for correct precision (e.g., "2m 3s" not "2m")
- Add displayHeight prop to ShellStatsBar so +N lines overflow
  calculation respects actual terminal height, not hardcoded DEFAULT_HEIGHT

* fix(cli): guard terminal progress bar against non-TTY stdout

Check process.stdout.isTTY in isProgressBarSupported() so escape sequences
are not emitted when stdout is piped, redirected to log files, or running
in CI environments where TERM_PROGRAM may be set but stdout is not a TTY.

Also add defensive isProgressBarSupported() guard in the effect cleanup.

* fix(cli): format tool elapsed time with minutes/hours for long-running tools

Previously showed raw seconds (e.g. "3600s") for long-running tools.
Now formats as "3s" for under a minute, "1m 30s" for minutes, and
"2h 15m" for hours, while keeping compact integer seconds for short
durations.

* fix(cli): audit fixes for terminal progress and shell output stats

Three issues found by post-merge audit:

- useTerminalProgress: WT_SESSION was wrongly used to exclude Windows
  Terminal. WT 1.6+ actually supports OSC 9;4 progress sequences (per
  Microsoft docs), so treat it as a positive indicator like iTerm2 and
  Ghostty.

- useTerminalProgress: add process.on('exit'|'SIGINT'|'SIGTERM') handler
  that writes PROGRESS_CLEAR. Without it, killing the CLI mid-tool (Ctrl+C,
  SIGTERM) left the terminal tab stuck showing an indeterminate progress
  indicator because React cleanup never ran. Mirrors the useBracketedPaste
  cleanup pattern.

- shell.ts: ANSI totalBytes used token.text.length (character count),
  inconsistent with the string path's Buffer.byteLength(..., 'utf-8').
  Multi-byte chars (CJK, emoji) now count as their true UTF-8 byte length
  in both paths.

* refactor(cli): right-align tool elapsed time, extract to its own component

Move the executing-tool elapsed-seconds indicator out of
ToolStatusIndicator (where it sat immediately after the spinner on the
left edge) and into a new right-aligned ToolElapsedTime component.

The left placement caused layout jitter: every second the elapsed text
width would change (e.g. "9s" → "10s" → "1m" → "1m 15s"), shifting the
tool name and description horizontally. Right-aligning the elapsed keeps
the tool name anchored and only the far-right timer moves.

- New packages/cli/src/ui/components/shared/ToolElapsedTime.tsx owns the
  setInterval + formatElapsed logic.
- ToolStatusIndicator is now pure status again; the executionStartTime
  prop is gone from it.
- ToolMessage and CompactToolGroupDisplay mount ToolElapsedTime as the
  last flex child of the status row, with marginLeft=1.
- ToolInfo gains flexGrow=1 so the description fills the middle and the
  timer sits flush at the right edge of the row.

* fix(core): measure tool elapsed from executing-transition, not validating-entry

trackedCall.startTime is stamped when a tool is first registered with the
scheduler (validating state), then preserved through awaiting_approval,
scheduled, and executing transitions. Using it for the executing-row
elapsed display meant any approval-wait time was counted as execution
time — a tool that waited 30s for user approval would flash "30s"
immediately when it actually began running.

Add a separate executionStartTime on ExecutingToolCall, stamped at the
moment of the transition into 'executing', and pipe that through
useReactToolScheduler into IndividualToolCallDisplay.executionStartTime.
startTime is kept as-is for durationMs bookkeeping.

Also stops piping executionStartTime for validating/scheduled states,
since those don't have a meaningful execution duration yet.

* fix(cli): only hook 'exit' for terminal progress cleanup, not SIGINT/SIGTERM

Registering SIGINT/SIGTERM handlers that neither re-raise nor exit
inhibits Node's default termination behavior. If this hook were ever the
only signal handler in play, Ctrl+C would leave the process hanging.

Drop the signal handlers and rely on 'exit' alone. Other parts of the
CLI already own the signal-to-shutdown path (gemini.tsx, telemetry
shutdown, sharedTokenManager, etc.) and ultimately call process.exit(),
which fires 'exit' and runs this cleanup. SIGKILL cannot be cleaned up
either way.

* fix(cli): thread executionStartTime through agent-view tool groups

The main TUI renders per-tool elapsed time via IndividualToolCallDisplay.
executionStartTime, but the agent-view adapter
(agentHistoryAdapter.ts) constructed its display items without this
field, so sub-agent tool groups never showed the elapsed indicator.

Thread it through the sub-agent event pipeline:

- AgentToolOutputUpdateEvent gains an optional executionStartTime,
  emitted once per callId by agent-core.onToolCallsUpdate the first time
  a call is seen in the scheduler's 'executing' state (carrying
  ExecutingToolCall.executionStartTime). This also fires for tools that
  produce no live output, so their elapsed indicator appears too.
- AgentInteractive tracks executionStartTimes in a callId→timestamp map,
  analogous to liveOutputs/shellPids. First TOOL_OUTPUT_UPDATE with a
  value wins; later events that re-carry it are ignored. Cleared on
  TOOL_RESULT.
- AgentChatView passes the map as the new fifth argument to
  agentMessagesToHistoryItems.
- The adapter reads the map for Executing tools and sets
  IndividualToolCallDisplay.executionStartTime, matching the main-view
  plumbing. Agent-view tool_groups now render the same elapsed-time
  indicator the main view does.

Adds three test cases covering set-when-executing, skip-when-completed,
and skip-when-map-absent.

* fix(core): skip stats accounting for string shell chunks

totalLines/totalBytes are only emitted alongside AnsiOutputDisplay in
the ANSI-array branch of updateOutput. Computing split('\n') and
Buffer.byteLength for string chunks was wasted work — the values never
left the function.

Only compute stats when event.chunk is an AnsiLine[] now.
2026-04-20 16:04:58 +08:00
易良
33d0b4af00
fix(core): normalize Windows PATH for MCP stdio servers (#3451)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(core): normalize Windows PATH for MCP stdio servers

* test(core): stabilize MCP stdio env assertion on Windows

* refactor(core): extract shared windowsPath utility and fix PATH override semantics

Address PR #3451 review comments:

1. [Critical] Fix PATH override semantics: normalize process.env before
   merging with server config so that a server-provided PATH fully
   replaces the parent value instead of being merged with a stale
   case-variant (Path).

2. [Suggestion] Extract mergeWindowsPathValues and
   normalizePathEnvForWindows into a shared utility module
   (src/utils/windowsPath.ts), eliminating near-identical copies in
   shellExecutionService.ts and mcp-client.ts. The shared version
   includes the fingerprint caching from shellExecutionService.

3. [Suggestion] Revert the "should connect via command" test to its
   original non-Windows form, restoring generic coverage. Add a
   dedicated test for server config PATH override behavior.

* fix(core): mock process.platform in shellExecutionService PATH tests

The shared windowsPath utility uses process.platform (not os.platform),
so the two Windows PATH normalization tests need to mock both to ensure
the utility detects the win32 platform correctly in test environments.

* fix(core): use objectContaining for env assertion in command transport test

On Windows, normalizePathEnvForWindows deduplicates PATH entries, so the
env passed to StdioClientTransport won't exactly match a raw process.env
spread. Use expect.objectContaining to verify server config env vars are
passed through without requiring an exact match on all platform-dependent
env keys.
2026-04-20 15:22:01 +08:00
顾盼
a82d766727
refactor(cli): replace slash command whitelist with capability-based filtering (Phase 1) (#3283)
* refactor(cli): replace slash command whitelist with capability-based filtering (Phase 1)

## Summary

Replace the hardcoded ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE whitelist with a
unified, capability-based command metadata model. This is Phase 1 of the slash
command architecture refactor described in docs/design/slash-command/.

## Key changes

### New types (types.ts)
- Add ExecutionMode ('interactive' | 'non_interactive' | 'acp')
- Add CommandSource ('builtin-command' | 'bundled-skill' | 'skill-dir-command' |
  'plugin-command' | 'mcp-prompt')
- Add CommandType ('prompt' | 'local' | 'local-jsx')
- Extend SlashCommand interface with: source, sourceLabel, commandType,
  supportedModes, userInvocable, modelInvocable, argumentHint, whenToUse,
  examples (all optional, backward-compatible)

### New module (commandUtils.ts + commandUtils.test.ts)
- getEffectiveSupportedModes(): 3-priority inference
  (explicit supportedModes > commandType > CommandKind fallback)
- filterCommandsForMode(): replaces filterCommandsForNonInteractive()
- 18 unit tests

### Whitelist removal (nonInteractiveCliCommands.ts)
- Remove ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE constant
- Remove filterCommandsForNonInteractive() function
- Replace with CommandService.getCommandsForMode(mode)

### CommandService enhancements (CommandService.ts)
- Add getCommandsForMode(mode: ExecutionMode): filters by mode, excludes hidden
- Add getModelInvocableCommands(): reserved for Phase 3 model tool-call use

### Built-in command annotations (41 files)
Annotate every built-in command with commandType:
- commandType='local' + supportedModes all-modes: btw, bug, compress, context,
  init, summary (replaces the 6-command whitelist)
- commandType='local' interactive-only: export, memory, plan, insight
- commandType='local-jsx' interactive-only: all remaining ~31 commands

### Loader metadata injection (4 files)
Each loader stamps source/sourceLabel/commandType/modelInvocable on every
command it emits:
- BuiltinCommandLoader: source='builtin-command', modelInvocable=false
- BundledSkillLoader: source='bundled-skill', commandType='prompt',
  modelInvocable=true
- command-factory (FileCommandLoader): source per extension/user origin,
  commandType='prompt', modelInvocable=!extensionName
- McpPromptLoader: source='mcp-prompt', commandType='prompt', modelInvocable=true

### Bug fix
MCP_PROMPT commands were incorrectly excluded from non-interactive/ACP modes by
the old whitelist logic. commandType='prompt' now correctly allows them in all
modes.

### Session.ts / nonInteractiveHelpers.ts
- ACP session calls getAvailableCommands with explicit 'acp' mode
- Remove allowedBuiltinCommandNames parameter from buildSystemMessage() —
  capability filtering is now self-contained in CommandService

* fix test ci

* fix memory command

* fix: pass 'non_interactive' mode explicitly to getAvailableCommands

- Fix critical bug in nonInteractiveHelpers.ts: loadSlashCommandNames was
  calling getAvailableCommands without specifying mode, causing it to default
  to 'acp' instead of 'non_interactive'. Commands with supportedModes that
  include 'non_interactive' but not 'acp' would be silently excluded.
- Apply the same fix in systemController.ts for the same reason.
- Update test mock to delegate filtering to production filterCommandsForMode()
  instead of duplicating the logic inline, preventing divergence.

Fixes review comments by wenshao and tanzhenxin on PR #3283.

* fix: resolve TypeScript type error in nonInteractiveHelpers.test.ts

* fix test ci
2026-04-20 14:34:43 +08:00
Edenman
6c999fe29f
feat(cli): add OAuth configuration flags to mcp add (#3442)
* feat(cli): Add OAuth redirect URI support to  command

- Add --oauth-redirect-uri, --oauth-client-id, --oauth-client-secret,
  --oauth-authorization-url, --oauth-token-url, and --oauth-scopes flags
  to the  command
- Enable configuration of custom OAuth redirect URIs for remote/cloud
  server deployments (fixes hardcoded localhost issue)
- Document auth.redirectUri in both developer and user-facing MCP docs
- Add comprehensive tests for OAuth configuration via CLI
- Update documentation with examples and guidance for remote deployments

Fixes #3336

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

* refactor(cli): harden OAuth flag handling in mcp add

- Reject combining --oauth-* flags with --transport stdio to surface the
  mistake instead of silently persisting an unused oauth config
- Rebuild OAuth config via single spread expression; drop the prior
  mutate-then-check pattern and the post-hoc enabled assignment
- Trim each scope token after comma split so "read, write" no longer
  stores leading/trailing whitespace
- Cover both new behaviors with tests; add missing --oauth-client-secret
  row and stdio-incompatibility note to the user MCP docs

* test(cli): use explicit Vitest/Yargs type imports in mcp add tests

Switch from namespace-style 'vi.Mock' and 'yargs.Argv' references to
explicit 'Mock' and 'Argv' imports, and replace the narrow
'(code?: number) => never' cast on the process.exit mock with
'typeof process.exit' so it tracks the current Node signature.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-20 14:12:17 +08:00
Shaojin Wen
9d8201d206
feat(core): PDF text extraction fallback and Jupyter notebook parsing (#3160)
* feat(core): add PDF text extraction fallback and Jupyter notebook parsing

For text-only models (qwen3-coder, deepseek) that lack PDF modality support,
read_file now falls back to pdftotext (poppler-utils) for text extraction
instead of returning an unsupported error. A new `pages` parameter enables
paginated PDF reading (e.g. "1-5", "3-").

Also adds structured .ipynb parsing — notebooks are displayed as labeled
cells with code blocks and execution outputs rather than raw JSON.

Key changes:
- New utils/pdf.ts: pdftotext integration with availability caching,
  page range parsing, 5MB maxBuffer, and 100K char output truncation
- New utils/notebook.ts: .ipynb JSON parser with per-cell output
  truncation (10K chars) and overall notebook truncation (100K chars)
- Modified fileUtils.ts: new 'notebook' FileType, PDF fallback logic,
  pages parameter threading
- Modified read-file.ts: pages parameter in schema/validation/execution

* fix(core): avoid circular dependency via shell-utils in pdf.ts

pdf.ts was importing execCommand from shell-utils.ts, which transitively
pulled in tool-utils.ts → ../index.js (barrel), creating a circular
dependency that caused AuthType to be undefined during vitest module
initialization in 46 test files.

Replace with a local execFile wrapper that has no transitive dependencies
beyond node:child_process.

* fix(core): use optional call on getContentGeneratorConfig

Moving the modalities computation outside the if-block caused
readManyFiles.test.ts to fail because its mock config doesn't implement
getContentGeneratorConfig — previously the method was only called for
media files (image/pdf/audio/video), never for text files.

Use ?.() to gracefully fall back to an empty modalities object when the
method is not defined.

* fix(core): reject open-ended PDF page ranges to enforce 20-page limit

Previously, parsePDFPageRange returned lastPage: Infinity for open-ended
ranges like "3-", which bypassed the 20-page validation check and caused
pdftotext to extract from the start page to EOF. This violated the
documented "Max 20 pages per request" contract.

Now validation explicitly rejects open-ended ranges with a helpful
message telling users to specify an explicit end page within the limit.
The pages parameter schema description and interface comment are also
updated to reflect this constraint.

* fix(core): tighten parsePDFPageRange to reject malformed tokens

parseInt() silently truncates invalid input, so values like "1-2-3",
"5abc", "1-2x", "1x-2", and "1.5" were accepted and then interpreted
as the wrong range (e.g. "1-2-3" parsed as 1-2). Switch to regex-based
whole-string validation so any non-matching input returns null at
ReadFileTool.build() time instead of reaching pdftotext.

* fix(core): surface processSingleFileContent errors in readManyFiles

readManyFiles previously dropped any file whose processSingleFileContent
result carried an error, so users only saw "No files matching the
criteria were found or all were skipped." This hid actionable guidance
such as the pdftotext-not-installed install hint, password-protected
PDF notices, and the >10MB size-limit message.

Now the per-file error message (already a human-readable string in
llmContent) is included as a content part, so batch reads surface the
same guidance as single-file reads.

* fix(core): tolerate whitespace around hyphen in parsePDFPageRange

The strict regex introduced in the previous commit stopped accepting
inputs like "1 - 5" or "3 -", which the old parseInt-based parser
handled (parseInt skips leading whitespace). Allow optional \s* on each
side of the hyphen while still rejecting malformed trailing tokens such
as "5abc" and "1-2-3".

* fix(cli,core): render failed @file reads as Error in atCommandProcessor

The previous commit surfaced per-file errors through readManyFiles, but
FileReadInfo still lacked a status field and atCommandProcessor
hardcoded ToolCallStatus.Success for every entry in result.files. So a
failed read (missing pdftotext, password-protected PDF, >10MB file)
rendered in the UI as if it had succeeded, just with the error text
embedded in the LLM content.

Add an optional `error` field on FileReadInfo, populate it in
readFileContent, and use it in atCommandProcessor to pick
ToolCallStatus.Error plus a resultDisplay string the user can see.

* fix(core): treat pdftotext maxBuffer overrun as truncation

When a text-dense PDF produced more than 5MB of stdout, Node killed the
child and `execFile` delivered the error as `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`,
which fell into the generic `pdftotext failed:` branch — so a perfectly
valid PDF failed instead of returning the usual truncated output.

Detect the maxBuffer error code in the execFile wrapper, and in
extractPDFText use the partial stdout with the existing truncation note.
Also lower the maxBuffer to 2×MAX_PDF_TEXT_OUTPUT_CHARS (from 5MB) since
anything past that is discarded anyway — this also caps RSS for
pathological inputs.

* fix(core): skip 10MB size gate for PDF text-extraction path

The generic 9.9MB file-size check ran before the pdf branch knew whether
we were taking the base64 inline path or the pdftotext text-extraction
path. That meant `read_file("huge.pdf", pages="1-5")` was rejected up
front even though pdftotext streams through the file and only emits a
capped (100K char) text slice — never loading 15MB into Node memory.

Move the size gate past the fileType/modalities decision point and skip
it when the PDF will go through text extraction (pages parameter set,
or model lacks pdf modality). The base64 inline path still carries its
own encoded-size cap, so oversized PDFs continue to be rejected there.

* fix(core): harden pdftotext wrapper against six audit findings

An adversarial pass over the PDF utilities turned up several issues
that warrant hardening before the PR lands:

- Argument injection (C1): filenames starting with `-` (e.g.
  `-opw=foo.pdf`) are parsed as options by poppler's argv parser when
  passed positionally. Insert `--` before `filePath` in both
  `extractPDFText` and `getPDFPageCount` so the shell's option parser
  stops processing flags. Reproduced locally: `pdftotext -h -` prints
  help while `pdftotext -- -h -` treats `-h` as the input file.

- Brittle availability signal (H1): `isPdftotextAvailable` used
  `stderr.length > 0` as the positive signal, so a sandbox that
  suppresses stderr would cache `false` for the whole process. Switch
  to the exit code.

- Concurrent availability probes (H2): N parallel callers (e.g. an
  `@`-glob of PDFs) each spawned their own `pdftotext -v` before the
  first probe resolved. Cache the in-flight promise.

- Precision-loss bypass of the 20-page cap (H3): `Number()` collapses
  any integer past 2^53 onto the same value, so the string
  `"999999999999999998-999999999999999999"` parsed as a 1-page range
  and slid past the validator. Cap accepted page numbers at 1,000,000.

- Timeout error clarity (M2): 30s timeouts surfaced as the generic
  `pdftotext failed:` branch with empty stderr. Detect SIGTERM/killed
  and emit a dedicated "timed out after 30s" message.

- Over-eager maxBuffer success (M1): the previous commit treated any
  maxBuffer overrun with non-empty stdout as a truncated success. If
  the overrun was driven by stderr spam (password warnings, corrupt-
  PDF diagnostics), that delivered garbage as success. Require at
  least MAX_PDF_TEXT_OUTPUT_CHARS of stdout before treating as
  truncated; otherwise re-run the password/corrupt detectors on the
  captured stderr.

Added regression tests for each.

* fix(core): gate non-regular files and oversized PDFs before extraction

Two defense-in-depth guards suggested by the adversarial audit:

- Non-regular files (FIFOs, sockets, /dev/zero, character devices)
  have meaningless `stats.size` (typically 0), so the 10MB size gate
  would happily wave them through. Handing `/dev/zero` to pdftotext
  then produced a 30s-timeout failure after the wrapper streamed
  megabytes into Node. Require `stats.isFile()` before routing into
  any extraction path.

- The previous commit skipped the 10MB gate for the PDF text-
  extraction path so `read_file("huge.pdf", pages="1-5")` could
  work. Unbounded, though, a multi-GB PDF would make pdftotext run
  until the 30s timeout fires. Add a separate 100MB ceiling for the
  extraction path with a guidance error pointing the user at `pages`
  or document splitting. The base64 inline path keeps its own encoded-
  size cap.

Added regression tests for both.

* fix(core): strip ANSI escapes and surface non-text outputs in notebooks

Two notebook-rendering issues surfaced by the audit:

- ipykernel emits ANSI CSI/SGR escape sequences (`\x1B[0;31m...`) in
  error tracebacks by default. Those codes add noise and burn tokens
  without conveying anything useful once we're rendering to plain
  text. Strip them from stream, execute_result, display_data, and
  error outputs.

- Cells whose only output was a non-text MIME type (image/png,
  text/html, application/vnd.jupyter.widget-view+json, ...) were
  silently dropped — the model saw the source code with no indication
  that a plot or HTML block existed. Emit a `[non-text output:
  <mime-types>]` placeholder so the model knows something was there
  without us inlining the payload.

* fix(core): round-2 audit fixes (in-flight cleanup, Windows timeout, ANSI/MIME)

Reverse audit on the previous three commits surfaced four medium-
severity issues plus a polish item:

- isPdftotextAvailable in-flight promise leak: the `.then(...)` cleared
  the cached promise on success but a synchronous throw inside the
  IIFE would have left a rejected promise stuck in the slot forever.
  Switch to `.finally` so the slot is always cleared.

- Timeout detection on Windows: Node's `execFile` `timeout` terminates
  via TerminateProcess on Windows, where `signal` is typically `null`
  rather than `'SIGTERM'`. The previous SIGTERM-only check would let
  Windows timeouts fall through to the generic "pdftotext failed"
  branch. Accept null/undefined signal alongside SIGTERM.

- ANSI regex was CSI-only: missed OSC hyperlinks (`ESC ]8;;url`),
  DCS, APC/PM/SOS, and lone two-byte escapes that ipykernel and
  related tools sometimes emit. Extend the pattern to cover all four
  families.

- Non-text MIME placeholder was attacker-controlled: a malicious
  notebook could set `data: {"\nIGNORE PREVIOUS INSTRUCTIONS\n": ...}`
  and that key would flow unescaped into `[non-text output: ...]`,
  smuggling prompt-injection payload bytes into the LLM context.
  Filter keys against the IANA MIME-type grammar before joining.

- Hoisted PDF_EXTRACTION_MAX_MB to module scope alongside the other
  size constants so it's discoverable in one place.

* chore(core): correct ANSI comment example and rename cache-reset test

Comment/test polish from the convergence audit:

- The `[@-Z\-_]` C1-Fe branch of the ANSI regex does not actually match
  `ESC c` (RIS), `ESC 7`, or `ESC 8`, which sit at 0x63/0x37/0x38. It
  does match IND/NEL/HTS/RI (ESC D/E/H/M). Correct the jsdoc example.

- The `should clear the in-flight promise after a probe to allow retries`
  test wasn't distinguishing the `.finally` behaviour from the
  `resetPdftotextCache()` call that immediately precedes the second
  probe. Rename it to reflect what it actually verifies; the `.finally`
  remains as defence-in-depth (a synchronous throw inside the IIFE's
  own handlers can't leave the in-flight slot stuck on a rejected
  promise).
2026-04-20 11:09:50 +08:00
ihubanov
0b8b3da836
feat(cli): add slashCommands.disabled setting to gate slash commands (#3445)
* feat(cli): add slashCommands.disabled setting to gate slash commands

Introduces a first-class way for operators to hide and refuse to execute
specific slash commands. Useful for multi-tenant / enterprise / sandboxed
deployments where different users should see different command subsets.

The denylist is sourced from three unioned inputs:

  * `slashCommands.disabled` settings key (string[], UNION merge), so
    workspace scopes can only add to a denylist set at user or system
    scope, never shrink it — matching the shape already used by
    `permissions.deny`.
  * `--disabled-slash-commands` CLI flag (comma-separated or repeated).
  * `QWEN_DISABLED_SLASH_COMMANDS` environment variable.

Matching is case-insensitive against the final (post-rename) command
name, so extension commands are addressable by their disambiguated
form (e.g. `firebase.deploy`). Disabled commands are removed from
`CommandService`'s output, so they disappear from autocomplete and
produce the standard unknown-command path in both interactive TUI and
non-interactive (`--prompt`) modes.

The scope of this change is slash commands only: it does not affect
tool permissions (still `permissions.deny`) or keyboard shortcuts.

* chore(cli): regenerate settings.schema.json for slashCommands.disabled

Regenerates the companion JSON schema consumed by the VS Code extension
after adding the `slashCommands.disabled` entry to the TS schema in the
previous commit. Required by the "Check settings schema is up-to-date"
CI lint step.

* fix(cli): route disabled slash commands to unsupported, not no_command

handleSlashCommand was passing the disabled denylist straight into
CommandService.create, so disabled commands disappeared from
`allCommands` too. The fallback existence check that distinguishes
"known but not allowed in non-interactive mode" from "truly unknown"
then failed, and disabled commands like `/help` fell through to
`no_command` — causing the caller to forward them to the model as
plain prompt text.

Keep `allCommands` unfiltered and apply the denylist only when
constructing the executable set and when producing the unsupported
response. A disabled command now returns `unsupported` with a
"disabled by the current configuration" reason and never reaches the
model. Added three regression tests covering the primary case,
case-insensitive match, and the preserved no_command path for
genuinely unknown input.
2026-04-20 11:06:26 +08:00
易良
7cded6e0df
feat(vscode-ide-companion): support /insight command (#2593)
* feat(vscode-ide-companion): support /insight command

Add ACP support for /insight progress streaming and report opening in the VSCode companion.

Resolves #2023

* fix(cli): defer insight command runtime deps

* test(cli): cover acp slash command allowlist

* Revert "test(cli): cover acp slash command allowlist"

This reverts commit 3209274ab6.

* Revert "fix(cli): defer insight command runtime deps"

This reverts commit 3b08491e46.

* Reapply "fix(cli): defer insight command runtime deps"

This reverts commit 386c5c67d3.

* Reapply "test(cli): cover acp slash command allowlist"

This reverts commit e2716140dd.

* refactor(cli): simplify insight ACP integration

- Replace `formatAcpInsightProgress` with `encodeAcpInsightProgress` using JSON payload
- Move imports to top-level, no longer defer loading for non-ACP mode
- Remove `INSIGHT_READY_MARKER` parsing from Session.ts as it's now handled by WebViewProvider

* refactor: extract insight protocol markers to core package

Move INSIGHT_PROGRESS_MARKER and INSIGHT_READY_MARKER from cli and
vscode-ide-companion packages to @qwen-code/qwen-code-core for better
shareability and to avoid duplication.

Also extract ACP_ALLOWED_COMMANDS constant in Session.ts to improve
readability and maintainability.

* refactor(vscode-ide-companion): extract test helper to reduce webview mock duplication

Introduce `setupAttachedProvider()` helper in WebViewProvider.test.ts
to eliminate ~160 lines of repeated webview mock + provider setup code
across 5 insight-related test cases.

* feat(cli): 添加ACP执行模式到内置命令

当ACP启用时,将executionMode参数传递给所有内置命令,
使命令能够识别当前运行在ACP模式下并相应地调整行为。

test(cli): 为insight命令添加ACP进度消息流测试

新增测试验证insight命令在ACP模式下能够正确流式传输
进度消息,而不必等待生成完成。测试涵盖了从开始到完
成的整个进度更新过程。

refactor(core): 重构insight协议消息格式

将insight进度和就绪消息从基于标记字符串的格式
改为结构化的JSON格式,提供更好的类型安全和解析
可靠性。

feat(vscode-ide-companion): 支持新的insight消息协议

更新WebViewProvider以支持新的结构化insight消息协
议,能够正确解析和处理来自CLI的进度和就绪消息。
```

* fix(vscode-ide-companion/insight): streamline insight progress handling

Trim redundant CLI insight coverage around the ACP path.

Keep the VS Code insight progress flow aligned with normalized slash commands and the updated progress layout.

* fix(insight): restore slash commands after webview reload

Cache available commands in the VS Code provider so webview restoration still exposes /insight without a manual login.

Also remove the unused progress bar markup to keep the UI diff smaller.

* Update packages/webui/src/index.ts

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

* fix(webui): remove duplicate insight card export

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-04-20 10:02:18 +08:00
易良
41f71ab7e7
feat(cli): add bare startup mode (#3448)
* feat(cli): add bare startup mode

Skip implicit startup discovery in bare mode while keeping explicit inputs such as include directories and extension overrides.

Add a repository plan document and targeted tests for config, startup, skills, extensions, and memory discovery.

* fix(bare): enforce explicit-only startup behavior

* fix(cli): preserve bare tools in non-interactive mode

* chore(docs): remove bare mode planning note
2026-04-20 10:01:59 +08:00
易良
cfe142e9a3
fix(vscode-ide-companion): preserve split stream ordering (#3450) 2026-04-20 10:01:31 +08:00
Shaojin Wen
60a6dfc14c
feat(cli): add session recap with /recap and auto-show on return (#3434)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): add session recap with /recap and auto-show on return

Users often open an old session days later and need to scroll through
pages to remember where they left off. This change adds a short
"where did I leave off" recap — a 1-3 sentence summary generated by
the fast model — so they can resume without re-reading the history.

Two triggers:
- /recap: manual slash command.
- Auto: when the terminal has been blurred for 5+ minutes and gets
  focused again (uses the existing DECSET 1004 focus protocol via
  useFocus). Gated on streamingState === Idle so it never interrupts
  an active turn. Only fires once per blur cycle.

The recap is rendered in dim color with a chevron prefix, visually
distinct from assistant replies. A new `general.showSessionRecap`
setting controls the auto-trigger (default on). /recap works
independent of the setting.

Implementation notes:
- generateSessionRecap uses fastModel (falls back to main model),
  tools: [], maxOutputTokens: 300, and a tight system prompt. It
  strips tool calls / responses from history before sending — tool
  responses can hold 10K+ tokens of file content that drown the recap
  in irrelevant detail. The 30-message window respects turn boundaries
  (slice never starts on a dangling model/tool response).
- Output is wrapped in <recap>...</recap> tags; the extractor returns
  empty (skips render) if the tag is missing, preventing model
  reasoning from leaking into the UI.
- All failures are silent (return null) and logged via a scoped
  debugLogger; recap is best-effort and must never break main flow.
- /recap refuses to run while a turn is pending.

* fix(cli): abort in-flight recap when showSessionRecap is disabled

If the user disables showSessionRecap while an auto-recap LLM call is
already in flight, the previous code returned early without aborting.
The pending .then would still pass its idle/abort guards and append the
recap, producing an unwanted message after the user has opted out.

Abort the controller and clear it eagerly so the resolved promise no
longer adds to history.

* fix(cli): gate /recap and auto-recap on streaming idle state

Two related issues from review:

1. /recap was only refusing when ui.pendingItem was set, but a normal
   model reply runs with streamingState === Responding and a null
   pendingItem. Invoking /recap mid-stream would generate a recap from
   a partial conversation and insert it between the user prompt and
   the assistant reply.

2. useAwaySummary cleared blurredAtRef before checking isIdle, so if
   focus returned during a still-streaming turn (after a >5min blur)
   the recap was permanently dropped — there was no later retry when
   the turn became idle, because isIdle was not in the effect deps.

Fixes:
- Expose isIdleRef on CommandContext.ui (mirrors btwAbortControllerRef
  pattern). Plumb it from AppContainer through useSlashCommandProcessor.
- recapCommand now refuses when isIdleRef.current is false OR
  pendingItem is non-null.
- useAwaySummary preserves blurredAtRef on the !isIdle bail and adds
  isIdle to the effect deps, so the trigger re-evaluates when the
  current turn finishes.
- Brief blurs (< AWAY_THRESHOLD_MS) still reset blurredAtRef.

Also seeds isIdleRef in nonInteractiveUi and mockCommandContext so the
new field has a sensible default outside the interactive UI.

* docs: document /recap command, showSessionRecap setting, and design

- User docs: add /recap to the Session and Project Management table in
  features/commands.md and a dedicated subsection covering manual use,
  the auto-trigger, the dim-color rendering, and the fast-model tip.
- User docs: add general.showSessionRecap row to the configuration
  settings reference.
- Design doc: docs/design/session-recap/session-recap-design.md covers
  motivation, the two trigger paths, the per-file architecture, prompt
  design with the <recap> tag and three-tier extractor, history
  filtering rationale (functionResponse can be 10K+ tokens), the
  useAwaySummary state machine, the isIdleRef gating for /recap, model
  selection, observability, and out-of-scope items.

* fix(core): exclude thought parts from session recap context

filterToDialog kept any non-empty text part, but @google/genai's Part
type also marks model reasoning with part.thought / part.thoughtSignature.
That hidden chain-of-thought was being fed to the recap LLM and could
get summarized as if it were user-visible dialogue.

Drop parts where either flag is set. Update the design doc's
History 过滤 section to call this out alongside the existing
tool-call/response rationale.

* docs(session-recap): correct debug-logging guidance, fill in state machine, sharpen UX wording

Audit of the session recap docs against the implementation found three
issues worth fixing:

- Design doc claimed debug logs were enabled via a QWEN_CODE_DEBUG_LOGGING
  env var. That var does not exist; debug logs are written to
  ~/.qwen/debug/<sessionId>.txt by default, gated by QWEN_DEBUG_LOG_FILE.
  Replace with the accurate path + opt-out behavior, and tell the reader
  to grep for the [SESSION_RECAP] tag.
- Design doc's useAwaySummary state machine table was missing the
  isFocused && blurredAtRef === null path (taken on first render and
  right after a brief-blur reset). Add the row.
- User doc's "Refuses to run ... failures are silent" line conflated the
  inline-error refusal with silent generation failures, and "(when the
  conversation is idle)" used internal jargon. Split the two cases and
  spell out what "idle" means, including the wait-then-fire behavior
  when focus returns mid-turn.

* docs(session-recap): correctly describe /recap vs auto-trigger failure modes

The previous wording said "Generation/network failures are silent — the
recap simply does not appear", but recapCommand returns a user-facing
info message ("Not enough conversation context for a recap yet.") in
exactly that path, and also returns inline messages for the
config-not-loaded and busy-turn guards.

Only the auto-trigger path is truly silent (it just skips addItem when
generateSessionRecap returns null). Split the two paths in the doc so
the manual command's "always responds with something" behavior is
distinguished from the auto-trigger's no-op-on-failure behavior.

* docs(session-recap): align prompt-rules section with the actual prompt

Two doc-vs-code mismatches in the design doc's "System Prompt" section,
caught with the same lens as yiliang114's failure-mode review:

- The bullet list claimed RECAP_SYSTEM_PROMPT forbids "推测用户意图"
  and "用 'you' 称呼用户". Those rules existed in an early draft but
  were dropped when the <recap> tag rules were added; the current
  prompt has no such restrictions. Replace with the actual rules and
  add a "与 RECAP_SYSTEM_PROMPT 一一对应" marker so future edits stay
  in sync.
- The doc said systemInstruction "覆盖" the main agent prompt. True
  for the agent prompt portion, but GeminiClient.generateContent
  internally calls getCustomSystemPrompt which appends user memory
  (QWEN.md / 自动 memory) as a suffix. Spell that out — the final
  system prompt is recap prompt + user memory, which is actually
  useful project context for the recap.

* docs(session-recap): translate design doc to English

The repo convention for docs/design is English (7 of 8 existing files;
auto-memory/memory-system.md is the only Chinese one). The first version
of this design doc followed the auto-memory example, which turned out
to be the wrong sample.

Translate to English while preserving the existing structure, the
state-machine table, the prompt-vs-doc 1:1 alignment, the
QWEN_DEBUG_LOG_FILE description, and the failure-mode notes added in
prior commits.

* fix(cli): drop empty info return from /recap interactive success path

The interactive success path inserts the away_recap history item
directly via ui.addItem and then returned `{type: 'message',
messageType: 'info', content: ''}`. The slash-command processor's
'message' case unconditionally calls addMessage, which adds another
HistoryItemInfo with empty text. The empty info renders as nothing
(StatusMessage early-returns null), but it still bloats the in-memory
history list and shows up in /export and saved sessions.

Return void on the interactive success path and on the abort path so
the processor's `if (result)` check skips the message-handler branch
entirely. Widen the action's return type to `void | SlashCommandActionReturn`
to match (same shape as btwCommand).
2026-04-19 21:38:48 +08:00
易良
528fcfcff8
feat(vscode-companion): enable Plan Mode toggle and approval UI (#2551)
* feat(vscode-companion): enable Plan Mode toggle and approval UI

- Add Plan Mode to the approval mode cycle (plan → default → auto-edit → yolo → plan)
- Add Tab key shortcut to cycle approval modes in the input field
- Fix cancel handling for exit_plan_mode: reject plan without aborting agent session
- Add plan approval UI in PermissionDrawer with markdown content rendering

Closes #1985

Made-with: Cursor

* fix(vscode-ide-companion/webview): finalize rejected plan prompts
2026-04-19 20:45:09 +08:00
jinye
9de33dded3
feat(cli): add /doctor diagnostic command (#3404)
Closes #3018
2026-04-19 19:25:55 +08:00
euxaristia
c175fd3d4a
feat(core): enhanced loop detection with stagnation + validation-retry checks (#3236) 2026-04-19 18:06:43 +08:00
Reid
28d5722955
fix(core): remove abort listener during cleanup (#3438) 2026-04-19 17:28:38 +08:00
gin-lsl
a02c115445
feat(tools): add Markdown for Agents support to WebFetch tool (#2734)
Closes #2025
2026-04-19 17:23:09 +08:00
euxaristia
9174c11cee
fix(ui): constrain shell output width to prevent box overflow (#2857)
* fix(ui): constrain shell output width to prevent box overflow

When shell commands produce wide table output (e.g., gh run list),
the text would overflow the bordered box container in the TUI because
AnsiOutputText didn't apply any width constraint.

This fix:
1. Adds maxWidth prop to AnsiOutputText component
2. Wraps output in MaxSizedBox for proper width/height constraints
3. Adds wrap=truncate to individual text tokens
4. Passes childWidth from ToolMessage (matching other renderers)

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

* fix(ui): address review feedback on AnsiOutput MaxSizedBox wrapping

MaxSizedBox requires its direct children to be row <Box> elements;
wrapping the rows in an extra <Box flexDirection="column"> broke the
layout contract and caused shell output to render as empty content.
Remove the wrapper so each line is a direct <Box> child of MaxSizedBox.

Update the "handles empty lines and empty tokens" test: with row
<Box> elements in place, empty AnsiLines are now correctly preserved
as blank output rows (matching the source terminal) instead of being
silently collapsed by the former <Text>-per-row rendering.

* test(ui): cover multi-token wide-line truncation in AnsiOutputText

The existing truncation test used a single 100-char token, which takes
the straightforward MaxSizedBox single-segment path. Real-world shell
output like `gh run list` is a single logical row composed of many
styled-column tokens whose combined width exceeds the box — that path
relies on per-token wrap="truncate" plus ink's flex layout for the
final crop, not MaxSizedBox itself. Cover that shape so future
regressions in either half of the mechanism are caught.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 15:42:52 +08:00
DennisYu07
eae247b50e
fix: display (#2766) 2026-04-19 15:25:29 +08:00
易良
8ad9a5b467
fix(cli): use live context for /btw side questions (#3429)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
2026-04-19 15:06:14 +08:00
Sharvil Saxena
6ebe28453d
fix(cli): /clear dismisses active /btw side-question dialog (#3431)
The /clear command cleared the history log but left an active /btw
side-question dialog visible in the fixed bottom area, because /btw
stores state in dedicated btwItem state (via setBtwItem) rather than
in history items. The ui.clear callback only called clearItems() and
clearScreen(), never cancelBtw(), so the pending-btw dialog survived.

Call cancelBtw() from ui.clear so /clear (and /reset, /new) abort any
in-flight btw request and null out the btwItem state.

Fixes #3334

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 14:59:20 +08:00
Reid
f7ebc372f1
feat(core): add dynamic swarm worker tool (#3433)
* feat(core): add dynamic swarm worker tool

  Add a swarm tool for ad-hoc parallel worker execution with bounded concurrency, wait-all and first-success modes, per-worker failure
  isolation, and aggregated results.

  Register the tool in core, prevent nested worker recursion, and document the new workflow.

* fix(core): harden swarm worker execution

  Prevent swarm calls from bypassing the outer scheduler concurrency budget.

  Disallow interactive question prompts in swarm workers by default, and avoid incomplete Markdown table escaping by using an HTML entity for
  pipe characters. Add focused tests for the scheduler behavior, worker tool restrictions, and result formatting.
2026-04-19 14:46:59 +08:00
Reid
cd8d9dce6a
fix(core): support older Git during repository initialization
Replace git init --initial-branch with git init followed by
  symbolic-ref HEAD refs/heads/main. This keeps new repositories on main
  without requiring Git 2.28 or newer.

  Also ensure checkpoint shadow repository setup uses its dedicated git
  config during the initial commit.
2026-04-19 14:24:01 +08:00
Shaojin Wen
4bf5bf22de
feat(cli): support refreshInterval in statusLine for periodic refresh (#3383)
* feat(cli): support refreshInterval in statusLine for periodic refresh

The statusLine (#3311) re-runs only when Agent state changes (token count,
model, git branch, etc.). Commands that display *external* data — a clock,
rate-limit counters, CI build status — have no Agent event to hook into
and go stale between messages.

Add an optional `ui.statusLine.refreshInterval` field (seconds, minimum 1)
that schedules a setInterval alongside the existing event-driven updates.
Overlap with state-change debounce is safe: `doUpdate` kills any in-flight
child and bumps the generation counter, so only the most recent output
reaches the footer.

Validation lives in `getStatusLineConfig`:
- Must be `number`, `Number.isFinite(...)`, `>= 1`
- Anything else is silently dropped (no interval scheduled)

No changes to the default behavior — configs without `refreshInterval`
behave exactly as before.

* fix(cli): yield periodic statusLine tick when previous exec is in flight

Review feedback on #3383: with `refreshInterval: 1` and a command whose
real exec time exceeds 1s, each tick was unconditionally calling
`doUpdate()` — which kills the in-flight child and bumps the generation
counter — so the prior exec's callback was always discarded as stale.
`setOutput` was never reached and the statusline stayed empty until
`refreshInterval` was removed or the command became faster.

Guard the interval callback with an `activeChildRef` check so a pending
exec is allowed to finish. State-change triggers (model switch, token
count, branch, etc.) still go through `scheduleUpdate` → `doUpdate`
directly and legitimately preempt stale children; only the periodic
tick yields. The existing 5s exec timeout is still the hard ceiling.

Also drop the redundant `'refreshInterval' in raw` check — the `typeof
raw.refreshInterval === 'number'` guard already excludes missing /
undefined values.

Tests:
- Add regression test `'skips periodic ticks while a previous exec is
  still running'` — three ticks during one unfinished exec trigger zero
  new spawns; the next tick after callback completion does spawn.
- Update two existing tests to resolve the mount exec before expecting
  subsequent ticks (the old tests implicitly relied on the starvation
  behavior being tolerated).

* test(cli): assert user-visible lines state in starvation regression

Self-review insight: the existing `skips periodic ticks while a previous
exec is still running` test only counted `exec` calls — it confirmed the
guard prevents redundant spawns, but would have silently passed even if
the eventual callback was still being discarded as stale (which is the
actual user-visible symptom of the starvation bug).

Add `expect(result.current.lines).toEqual(['done'])` after resolving the
mount's pending callback. Without the guard, generationRef would have
bumped 3 times during the yielded ticks, the callback's captured gen
would fail the stale check, `setOutput` would never fire, and `lines`
would stay empty — now caught explicitly.

* perf(cli): dedupe statusLine output to skip unchanged Footer re-renders

Review feedback on #3383 (narrow terminal stacking): when
`refreshInterval` fires at 1s and the command output is unchanged, the
mount-and-setOutput cycle still allocates a new array and triggers a
Footer re-render. Under certain narrow-terminal conditions, Ink's
erase-line accounting mis-counts wrapped rows and stale content
accumulates on screen.

The Footer-layout root cause is in #3311's narrow-mode flex setup and
Ink's truncate semantics, which is out of scope for this PR. But we
can cut the re-render surface here by preserving the `lines` array
reference when the command produces identical output — a strict
Pareto improvement for any caller (clock-style statuslines with
second-precision still re-render; rate-limit / branch / CI-status
style statuslines that change infrequently stop triggering work every
tick).

Tests:
- `preserves the same lines array reference when output is unchanged`
  asserts referential equality after a re-exec with identical stdout.
- `produces a new reference when output changes` guards against
  over-eager dedup that would miss legitimate updates.

* fix(cli): stabilize Footer rendering in narrow terminals

Narrow-terminal E2E feedback on #3383: with `refreshInterval` at 1s,
empty lines were accumulating above the input prompt each tick. Root
cause is in the Footer flex layout — originally from #3311 — where Ink
miscounts logical rows vs the physical rows the terminal actually uses.

Two adjustments, both idiomatic (used elsewhere in the repo already):

1. Left column — `minWidth={0}`. Without this, Yoga's `min-width: auto`
   default keeps the Box at its natural content width, so a statusline
   wider than the terminal doesn't engage `<Text wrap="truncate">`; the
   text renders at content-width and the terminal wraps it physically.
   `minWidth={0}` lets the column shrink so the text child can truncate
   at container width.

2. Right section — `flexWrap="wrap"`. With multiple indicators (sandbox
   label, debug badge, dream, context-usage) the row can exceed a narrow
   terminal's width. Without `flexWrap` Ink lays them out in a single
   logical row, but the terminal physically wraps to two — Ink's erase
   sequence (`\e[2K\e[1A…` per logical row) then clears one row while
   two exist, and the extra row ghosts every re-render. With `wrap` Ink
   tracks the second row explicitly and erases correctly.

Together these make the Footer's row count match between Ink's logical
view and the terminal's physical view, so frequent re-renders (as
`refreshInterval` enables) stop accumulating ghost rows.

Needs verification in a real narrow TTY — from this environment I can
reason about the flex semantics and confirm both props are supported by
Ink's Box, but actually observing ghost-row elimination requires
process.stdout.columns on a real terminal.

* Revert "fix(cli): stabilize Footer rendering in narrow terminals"

This reverts commit 9758cda85f. Reason: I could not reproduce BZ-D's
reported ghost-row stacking in tmux (40x25, 2-line statusline + real
exec + Static history + refreshInterval: 1) over 14+ ticks. Both
`minWidth={0}` and `flexWrap="wrap"` are legitimate defensive idioms,
but without a failing repro I can't verify they address the reported
bug, and I shouldn't ship a speculative layout change as "the fix".

Keeping the output-dedup commit (e1d321186) — that one is a strict
improvement regardless of the underlying Ink behavior. Will request
BZ-D's specific terminal setup and reopen with a verified fix (or
confirm the issue is specific to a particular emulator, not flex/Ink).
2026-04-19 11:12:16 +08:00
Shaojin Wen
f340d95446
ci(stale): enable 35+35 stale/close policy for pull requests (#3375)
* ci(stale): enable 28+28 stale/close policy for pull requests

- Fix the repository guard so the workflow actually runs on
  QwenLM/qwen-code (it was previously gated to google-gemini/gemini-cli
  and never executed in this repo).
- Scope the behavior to pull requests for now; issue policy will be
  introduced separately once triage labels are in place.
- Mark a PR stale after 4 weeks without activity, then close it after
  another 4 weeks.
- Exempt pinned, security, status/blocked, status/on-hold, and
  status/ready-for-merge from auto-close.
- Remove the stale label automatically when activity resumes, and
  process the oldest PRs first on each run.

* ci(stale): loosen PR cadence from 28+28 to 35+35

Five weeks + five weeks gives contributors more slack around holidays
and busy periods, and reduces the first-run impact on the existing
backlog. The total window moves from 56 days to 70 days.

* ci(stale): move cron from 01:30 UTC to 00:30 UTC

Shift by one hour so results are ready before the Beijing work day
starts (08:30 local), while still avoiding the top of the hour (the
high-contention window for GitHub-hosted runners) and staying 30
minutes after release.yml at 00:00 UTC.

* ci(stale): drop redundant repo guard and document ops-per-run

- Remove the `github.repository == 'QwenLM/qwen-code'` job guard:
  scheduled runs are already disabled on forks by GitHub, and
  workflow_dispatch is manually-triggered so the guard adds no safety.
- Add a comment explaining the `operations-per-run: 100` rationale
  (rate-limit headroom given the ~150-PR backlog).
2026-04-19 09:45:17 +08:00
euxaristia
a623655c8f
fix(build): invoke tsx directly via node --import instead of npx (#3237)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(build): invoke tsx directly via node --import instead of npx

npx resolution breaks when scripts/build.js is invoked under bun
(bun's npx wrapper intercepts and runs tsx inside bun's runtime, where
tsx's CJS entry fails to resolve). Using 'node --import tsx/esm' skips
the npx layer entirely and works under both npm and bun invocation.

* fix(build): use node --import tsx/esm for generate:settings-schema script

Matches the approach taken in scripts/build.js so running
`bun run generate:settings-schema` directly bypasses bun's npx wrapper
and avoids the `Cannot find module './cjs/index.cjs'` tsx CJS failure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 03:14:13 +08:00
jinye
afa7fc3855
feat(cli): add early input capture to prevent keystroke loss during startup (#3319)
* feat(cli): add early input capture to prevent keystroke loss during startup (#3224)

Start raw mode stdin listening immediately after setRawMode(true), buffer
user input during REPL initialization (200-500ms), then replay it once
KeypressProvider is mounted. Prevents keystrokes typed before the REPL
is ready from being silently dropped.

- Filter out terminal response sequences (DA, DA2, OSC, DCS, APC)
  while preserving real user input (arrow keys, function keys, etc.)
- 64KB buffer limit for safety
- Replay via setImmediate() to ensure subscribers are registered first
- Disable via QWEN_CODE_DISABLE_EARLY_CAPTURE=1
- Add benchmark-startup.sh / benchmark-startup-simple.sh for baseline
  startup time measurement

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

* fix(cli): fix bugs and optimize early input capture

- Fix getAndClearCapturedInput resetting captured flag, preventing potential re-arm
- Fix passthrough mode replay bypassing paste marker handling in KeypressContext
- Optimize buffer storage from O(n^2) concat to chunked collection
- Optimize filterTerminalResponses to use pre-allocated Buffer instead of number[]
- Add atomic stopAndGetCapturedInput API to prevent two-step usage errors
- Remove unrelated benchmark shell scripts
- Add test for stopAndGetCapturedInput

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

* fix(cli): fix listener leak, silent failures, and error handling in early input capture

- Register cleanup for stdin listener in gemini.tsx to prevent orphaned
  listener on any error path before UI mounts
- Add try-catch and cancellation guard to setImmediate replay in
  KeypressContext to handle component unmount and replay errors gracefully
- Stop capture immediately and warn when buffer limit is reached instead
  of silently dropping data with a debug-level log
- Capture stdin reference at registration time so removeListener always
  operates on the correct stream instance
- Add debug log when early capture is skipped due to non-TTY stdin

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

* fix(cli): fix early input capture being lost under React StrictMode

Move stopAndGetCapturedInput() from inside KeypressProvider's useEffect
to before render() in startInteractiveUI. When DEBUG=1, React StrictMode
deliberately runs effect→cleanup→effect, causing the first mount to drain
the buffer and schedule a replay that the cleanup immediately cancels. The
second mount found an empty buffer, silently discarding startup keystrokes.

By draining once before render() and passing the bytes as a stable prop,
StrictMode remounts always read the same data and can schedule replay on
the second (stable) mount.

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

* fix: handle split ESC prefixes in early input capture

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

* fix: conditionally flush pending startup capture bytes

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

* fix: drop incomplete escape sequences instead of replaying as user input

When capture stops with an incomplete ESC sequence in pendingTerminalResponse
(e.g. lone \x1b or \x1b[), classifyEscapeSequence returns 'incomplete'.
Previously shouldReplayPendingAtStop used !== 'terminal' which treated
incomplete sequences as user input. Changed to === 'user' so only
definitively-user input is replayed; ambiguous sequences are safely dropped.

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>
2026-04-19 00:40:44 +08:00
易良
56e82279c4
perf(vscode): fix input lag in long conversations (#2395) (#2550)
Extract message list into a React.memo component to prevent
re-rendering the entire chat history on every keystroke.

- Extract MessageList as a memoized component
- Wrap UserMessage, AssistantMessage, ThinkingMessage with React.memo
- Stabilize onFileClick callback with useCallback
- Remove console.log from render path
- Wrap handleToggleThinking with useCallback

Fixes #2395

Made-with: Cursor
2026-04-18 23:43:46 +08:00
易良
cd1be1c524
feat(vscode-ide-companion): add agent execution tool display (#2590)
Preserve structured agent rawOutput through the VSCode session pipeline.

Render dedicated agent execution cards from shared webui components.
2026-04-18 23:39:26 +08:00
Edenman
4ee9ca912c
feat(mcp): add OSC 52 copy hotkey for OAuth authorization URL (#3337) (#3393)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
When MCP OAuth authentication falls back to the "copy this URL into
your browser" path (e.g. remote/web terminal where the browser can't
auto-open), long URLs wrap across lines inside the bordered dialog and
the trailing │ border characters get selected alongside the URL,
forcing the user to manually strip them out before pasting.

Surface the URL on a dedicated event and let the user press 'c' to
push it to the local clipboard via an OSC 52 escape sequence. Works
through SSH and modern web terminals (iTerm2, Windows Terminal,
xterm.js-based emulators, tmux with set-clipboard, etc.) without a
subprocess, and falls back to a visible "copy the URL above manually"
hint when the terminal is not a TTY or OSC 52 is blocked.

Key points:
- OAuth provider emits OAUTH_AUTH_URL_EVENT carrying the full URL.
- AuthenticateStep listens, tracks it in state, and binds 'c' while
  authenticating (modifier/paste keys are filtered out).
- copyToClipboardViaOsc52 writes to stderr when it's a TTY,
  falls back to stdout, and wraps the sequence for tmux/GNU screen
  via DCS passthrough so multiplexed sessions still work.
- Honest feedback: distinct "copy request sent" / "cannot write to
  terminal" states with a short auto-revert so repeated presses reset
  the timer.

Fixes #3337
2026-04-18 20:22:06 +08:00
Reid
9f7f061bcc
fix(cli): wait for dual output stream shutdown (#3416)
Make DualOutputBridge.shutdown() await the underlying write stream close
  event instead of returning immediately after stream.end(). This removes
  the Windows temp directory cleanup race in DualOutputBridge tests and
  makes interactive cleanup reliably flush session_end.
2026-04-18 18:11:19 +08:00
Reid
7eba1c4635
test(core): update scheduler registry mock (#3415)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Update the CoreToolScheduler retry-loop test registry mock to match the current
  ToolRegistry interface. Add ensureTool and getAllToolNames so the tests exercise
  the scheduler path used in production.
2026-04-18 13:46:46 +08:00
Viktor Szépe
a1d1e5e276
Fix typo in class name (#2189) 2026-04-18 11:59:36 +08:00
jinye
9f4734e84d
fix(tool-registry): add lazy factory registration with inflight concurrency dedup (#3297)
Closes #3221.

Introduces a lazy factory API on ToolRegistry (registerFactory,
ensureTool, warmAll, getAllToolNames) as infrastructure for future
esbuild code-splitting (#3226). With the current single-bundle build,
the lazy API does not change startup time on its own — the primary
immediate value is fixing three pre-existing bugs uncovered while
designing it.

Bug fixes:

- Concurrent instantiation (P0): the original ensureTool had no
  concurrency protection around `await factory()` — two concurrent
  calls for the same tool both passed the cache check and each ran the
  factory, producing two instances. AgentTool and SkillTool register
  SubagentManager listeners in their constructors, so the extra
  instance leaked listeners. Fix: a per-name `inflight: Map<string,
  Promise<Tool>>` so concurrent ensureTool() calls share a single
  promise. On factory rejection the inflight entry is cleared so a
  subsequent call can retry.

- stop() resource leak: stop() only disposed tools already in
  `this.tools`; tools still loading in `inflight` when stop() ran
  finished afterward and were never disposed. Fix: await
  Promise.allSettled(inflight.values()) before the dispose loop.

- Cache hit left stale factory: ensureTool's cache-hit branch did not
  delete the factory entry, so warmAll() would re-invoke the factory
  for an already-loaded tool. Fix: delete the factory on cache hit.

Additional hardening in response to review feedback:

- warmAll({ strict?: boolean }): strict mode re-throws the first
  factory failure rather than swallowing it. Config.initialize() uses
  strict: true so a broken built-in tool fails startup fast instead of
  silently leaving a partially initialized registry; runtime-path
  callers (GeminiChat, agent runtime, etc.) continue to use the
  non-strict default and log failures via debugLogger.
- getAllTools() and getFunctionDeclarationsFiltered() emit a debug
  warning when called while unloaded factories remain, nudging callers
  toward warmAll() without hard-breaking existing code paths.
- copyDiscoveredToolsFrom() now iterates source.tools.values()
  directly instead of source.getAllTools() — the copy path deals only
  with already-discovered MCP/command tools and should not trigger the
  unloaded-factory warning.
- MemoryTool and SkillTool config parsing was extracted into
  memory-config.ts and skill-utils.ts so a factory can resolve tool
  metadata without importing the tool module.

Tests:

- tool-registry.test.ts adds 128 lines covering: concurrent ensureTool
  runs the factory exactly once, warmAll and ensureTool overlap,
  retries succeed after a prior factory failure, stop() disposes tools
  that finish loading after stop was called, and warmAll strict vs
  default behavior.
- 33 existing call sites across cli, core, agents, and subagents were
  updated to await warmAll() before bulk tool access.
2026-04-18 10:31:50 +08:00
euxaristia
5facd8738b
feat(core): detect tool validation retry loops and inject stop directive (#3178)
Primary change: prevent the model from burning tokens in an infinite retry
loop when a tool call repeatedly fails schema validation with the same
error (observed with ask_user_question and a malformed `questions`
parameter retrying 10+ times with the same validation error).

- Track consecutive validation failures per (tool name, error message)
  pair in CoreToolScheduler via a `validationRetryCounts` Map.
- After 3 consecutive failures for the same (tool, error) pair, append a
  RETRY LOOP DETECTED directive to the error response instructing the
  model to stop, re-examine the schema, try a fundamentally different
  approach, or surface the issue to the user.
- Reset per-tool counters when the tool invocation succeeds; reset
  globally when an incoming batch shares no tool name with any
  previously failing tool; reset the per-tool counter when the tool
  returns a different validation error so unrelated mistakes do not
  accumulate toward the threshold.
- Distinct from LoopDetectionService, which tracks model-behavior loops
  (repeated thoughts, stagnant actions); this change catches tool-API
  misuse loops at the scheduler layer.

Piggyback fixes bundled in the same PR:

- packages/cli/index.ts, packages/core/src/services/shellExecutionService.ts:
  treat PTY `EAGAIN` on the read path as an expected read error alongside
  `EIO`, avoiding noisy surface-level failures from transient
  non-blocking reads.
- scripts/build.js: switch the settings-schema generation step from
  `npx tsx` to `node --import tsx/esm` for Bun compatibility.

Tests:

- Unit tests in coreToolScheduler.test.ts cover: directive injection on
  the 3rd consecutive failure, counter reset when a different tool is
  called, and counter reset after a successful invocation of the same
  tool (fail → fail → succeed → fail → fail must not trip the directive).
2026-04-18 10:24:46 +08:00
jinye
699cb05206
fix(cli): auto-submit on number key press in AskUserQuestionDialog (#3407)
Fixes #500.

Number keys in AskUserQuestionDialog previously only moved the highlight
cursor without submitting, inconsistent with RadioButtonSelect and the
standard tool approval dialog. Users pressed a number, saw the option
highlight, and assumed it was selected, but the dialog was still waiting
for Enter.

- For single-select predefined options, pressing a number key now
  auto-submits immediately.
- Multi-select, "Other" custom input, and the Submit tab remain
  highlight-only (unchanged).
- Extracted a shared selectAndAdvance helper to deduplicate the
  select-and-submit/advance logic across 4 code paths (number key,
  Enter, multi-select submit, custom input submit).
- Removed redundant isFocused guard inside the useKeypress callback;
  it is already handled via the isActive parameter.

Tests cover all four behavioral branches: single-select auto-submits,
multi-select does not, "Other" custom input does not, and the Submit
tab does not.
2026-04-18 10:03:32 +08:00
chinesepowered
ed6f9e056e
fix(sdk): settle pending next() promise in Stream.return() to prevent hangs (#2981)
* fix(sdk): settle pending next() promise in Stream.return() to prevent hangs

* test(sdk): add regression tests for Stream.return() pending-promise cleanup
2026-04-18 09:46:56 +08:00
chinesepowered
b82ad2bd4c
fix(channels): re-attach bridge disconnect handler after crash recovery (#2975) 2026-04-18 09:42:32 +08:00
chinesepowered
9a420d0fce
fix(weixin): check full 4-byte PNG magic signature (#2970)
PNG's magic bytes are 89 50 4E 47, but detectImageMime only checked
the first three. The WebP branch in the same function correctly checks
all four bytes of its signature — the PNG path was clearly an oversight.
Extend the PNG check to include 0x47 ('G') for consistency and to
eliminate the (admittedly rare) false-positive window.
2026-04-18 09:32:58 +08:00
chinesepowered
c012462514
fix(text-buffer): unify offset-to-position logic (#2969)
getPositionFromOffsets used different per-line length calculations and
different comparison operators for start vs end offsets, producing
asymmetric and sometimes invalid results at line boundaries.

Concrete failure: lines=['abc','def'], endOffset=4 (the position after
the newline at offset 3). The start calc correctly resolved this to
(row=1,col=0), but the end calc used lineLength=length (no +1 for the
last-line case) combined with >= and returned (row=0,col=4) — an
out-of-bounds column on a 3-char line.

Downstream, replaceRangeInternal rejects endCol > currentLineLen(endRow)
as an invalid range and silently returns state unchanged. This caused
vim line-change commands (vim_change_movement 'j'/'k', vim_change_line
spanning row boundaries) to no-op while still pushing an empty undo
frame.

Replace both loops with a single offsetToRowCol helper that matches the
original start-calc logic, and update the vim 'change multiple lines
down' test whose expectation was baked around the silent no-op.
2026-04-18 09:27:12 +08:00
chinesepowered
db4b76576a
fix(scripts): avoid 'undefined Options: ...' for enums without description (#2963)
schema.description is only assigned when setting.description is truthy.
For enum settings missing a description, the subsequent += produced the
literal string 'undefined Options: foo, bar' in the generated JSON
schema. Initialize the field when absent instead of concatenating onto
undefined.
2026-04-18 09:19:23 +08:00
chinesepowered
fc75913e50
fix(integration-tests): honor stdinDoesNotEnd option (#2966)
The stdinDoesNotEnd option was completely broken. The original code had
a conditional stdin.end() scoped to object-type promptOrOptions, followed
by an unconditional stdin.end() that always ran — so stdinDoesNotEnd: true
had no effect.

Restructure as an explicit keepStdinOpen check: close stdin unless the
caller passed an options object with stdinDoesNotEnd: true. The string-
prompt call path still closes stdin, and null is guarded (typeof null
=== 'object' in JS).
2026-04-18 09:17:27 +08:00
chinesepowered
f525fa30a3
fix(scripts): remove duplicate bundle rmSync in clean script (#2964)
scripts/clean.js deleted the bundle directory twice. The second call
was harmless (the first already removed it) but clearly a copy-paste
leftover from when RMRF_OPTIONS was introduced.
2026-04-18 09:13:34 +08:00
chinesepowered
28f8cb3e20
fix(sandbox): fall back to 'latest' tag when image name has no colon (#2962)
If the sandbox image name has no explicit :tag and QWEN_SANDBOX_IMAGE_TAG
is unset, imageName.split(':')[1] returns undefined, producing a bogus
build target like 'myimage:undefined'. Fall back to 'latest' to match
Docker's conventional default.
2026-04-18 09:07:05 +08:00
chinesepowered
82ba569e1b
fix(dingtalk): remove reactionContext map to eliminate leak from blocked messages (#2979) 2026-04-18 08:50:45 +08:00