* 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.
* 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`.
* 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.
* 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
* 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>
* 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.
* 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.
* 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
* 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>
* 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).
* 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.
* 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>
* 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
* 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).
* 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
* 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>
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>
* 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.
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.
* 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).
* 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).
* 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>
* 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>
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
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
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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.