mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
2636 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
12b26ba063
|
feat(cli): add Traditional Chinese (zh-TW) as a UI language option (#3569)
* feat(cli): add Traditional Chinese (zh-TW) as a UI language option
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: use upstream unused-keys-only-in-locales.json to resolve conflict
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* revert: remove check-i18n.ts changes to avoid pre-existing zh.js issues
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(cli): add Traditional Chinese (zh-TW) as a UI language option
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): add WITTY_LOADING_PHRASES to zh-TW locale
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): sync zh-TW.js with en.js keys, fix double-escape, fix check-i18n.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: resolve conflict in unused-keys-only-in-locales.json
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): add missing Performance translation to zh-TW
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): add quotes to Performance key in zh-TW
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): regenerate zh-TW.js with correct multi-line value parsing
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: resolve conflict in unused-keys-only-in-locales.json
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): regenerate zh-TW.js with correct multi-line value parsing
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): standardize zh-TW.js key quoting and sync zh.js keys
- Convert zh-TW.js keys from double-quoted to single-quoted to match en.js style
- Fix zh.js key mismatches: add missing keys (Value:, No server selected, prompts, required, Enum) and remove extra keys (The name of the extension to update, Session (temporary))
- Regenerate unused-keys-only-in-locales.json
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): update loading phrases when UI language changes
Add getCurrentLanguage() to useMemo deps in usePhraseCycler so that
WITTY_LOADING_PHRASES re-evaluates after a /language switch instead of
staying locked to the language active at mount time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(i18n): normalize locale separators and fix case-insensitive language lookup
- detectSystemLanguage(): normalize POSIX locales (e.g. zh_TW.UTF-8 → zh-tw)
by replacing underscores with hyphens and lowercasing before matching, so
users with LANG=zh_TW.UTF-8 correctly detect zh-TW instead of falling
through to zh
- getLanguageNameFromLocale(): compare codes case-insensitively so that
normalizeOutputLanguage('zh-TW') resolves to 'Traditional Chinese' instead
of falling back to 'English'
- Add test cases for zh-TW / zh-tw / ZH-TW in normalizeOutputLanguage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): update getLanguageNameFromLocale mock to include zh-TW
Add 'zh-tw' entry to the mock map and normalize locale input with
toLowerCase() so the mock mirrors the real case-insensitive implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
609b4324f6
|
perf(core): cut runtime sync I/O on tool hot path by 91% (#3581)
* perf(core): make chat recording writes async
Every recorded chat event (user message, assistant turn, tool call,
tool result, slash command, etc.) was issuing 4 sync fs syscalls on
the main event loop: existsSync(dir) + mkdirSync(dir) + existsSync(file)
+ appendFileSync(file). For a tool-heavy prompt this added ~88 sync
I/O calls per session, blocking the UI render and keypress handler
during each one.
- chatRecordingService.appendRecord: cache ensure-flags so dir/file
creation runs once per session, then enqueue the actual write on a
per-instance promise chain (writeChain). lastRecordUuid is updated
synchronously so chained createBaseRecord still sees the right
parentUuid without waiting for the previous write.
- chatRecordingService.flush: drains the chain — wired into
Config.shutdown so no records are lost on exit.
- jsonl-utils.writeLine: now actually async (fs.promises.mkdir +
fs.promises.appendFile) with per-dir mkdir cache. The existing
per-file mutex still serializes writes correctly.
- Tests updated to await flush() before assertions.
Trace measurement on a single tool-heavy prompt: 110 → 20 sync I/O
calls (-82%), with chatRecordingService dropping from 88 to 0.
* perf(core): cache repeated fs lookups on tool hot path
Each tool invocation went through validatePath → isPathWithinWorkspace
→ fullyResolvedPath, plus its own existence/dir checks. The same paths
got re-resolved across back-to-back tool calls, and ripGrep re-
discovered .qwenignore on every Grep.
- workspaceContext.fullyResolvedPath: bounded LRU on input path
(1024, FIFO). Failed resolutions are NOT cached so retries work.
- paths.validatePath: cache positive isDirectory results; ENOENT
falls through every time so a freshly created file is picked up
immediately.
- ripGrep: module-level caches for searchPath-is-dir and per-dir
.qwenignore presence (256 each, FIFO).
- fileUtils.processSingleFileContent: drop the existsSync gate;
let fs.promises.stat throw ENOENT and convert to FILE_NOT_FOUND
in catch.
Trace: 20 → 10 sync I/O calls. Cumulative reduction since the
chat-recording change: 110 → 10, -91%. All 6057 core tests pass.
* test(core): cache reset hooks + regression-guards from audit
Self-review pass on the previous two perf commits surfaced a few
follow-ups worth pinning down before they bite:
- Module-level caches (paths.isDirectoryCache, ripGrep dirIsDir/qwen-
Ignore, jsonl-utils.ensuredDirs) persisted across vitest cases
silently. Added underscore-prefixed `_reset*ForTest` exports and
wired one into the validatePath describe block so future cases
mutating the same absolute paths can't pass by accident.
- Documented the parentUuid-chain tradeoff on chatRecordingService
.appendRecord: when the async write rejects, lastRecordUuid was
already set sync, so subsequent records reference an absent
ancestor — readers like sessionService.reconstructHistory then
silently drop those descendants. Same observable failure mode as
the prior sync code's caught-and-logged throw.
- Documented the dir<->file mutation and mid-session .qwenignore
staleness windows for the validatePath / ripGrep caches.
- Added regression tests:
* validatePath does NOT cache ENOENT (Edit-then-Read works)
* validatePath skips re-stat on cache hit (perf assertion)
* flush() resolves immediately on a fresh service
* a rejected writeLine does not block the next record
Full core suite: 6061 pass, 2 skipped — no regressions.
* fix(core): cache chatsDirEnsured only on mkdir success
Pre-fix, the flag flipped to true even when mkdirSync threw, so a single
transient failure (NFS EACCES, sandbox mount race, parent dir briefly
missing) would short-circuit every subsequent appendRecord and silently
drop the rest of the session's transcript with no error surfaced.
Reported by zhangxy-zju on #3581.
* fix(cli): destroy stdout instead of process.exit on EPIPE
Routine CLI patterns like `qwen -p ... | head -1` / `| less` / `| grep -m1`
close the downstream pipe and trigger EPIPE. The previous handler called
process.exit(0), which bypassed the caller's runExitCleanup -> Config
.shutdown -> chat-recording flush() chain and silently dropped queued
JSONL writes (most recent assistant turn + tool results).
Destroying stdout instead lets writes fail fast and the natural function
return drive cleanup. We deliberately do not also abortController.abort()
here: the abort path runs handleCancellationError which itself calls
process.exit(130), re-introducing the same bypass.
Reported by zhangxy-zju on #3581.
* fix(cli): bound runExitCleanup with per-fn + wall-clock timeouts
Pre-fix, runExitCleanup was an unbounded series of awaits. After the
async-jsonl change moved chat-recording writes off the calling thread
(Config.shutdown now `await flush()`s the queue), any hung syscall
(slow disk, dead NFS mount, stuck MCP socket, telemetry HTTP stall)
would hang process exit indefinitely — sync writes were inherently
bounded by syscall return; async writes are not.
Adds per-cleanup 2s + overall 5s wall-clock failsafes on the same
shape as Claude Code's gracefulShutdown.ts. Also replaces dead
test-isolation code (`global['cleanupFunctions']` was never on global,
the array is module-private) with a `_resetCleanupFunctionsForTest`
hook matching the convention from
|
||
|
|
44b482928b
|
chore(release): bump version to 0.15.2 (#3596)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Update version from 0.15.1 to 0.15.2 across all packages and lockfile |
||
|
|
5e1b8b0d59
|
feat(vscode-companion): support /export session command (#2592)
* feat(vscode-companion): support /export session command * fix(vscode-ide-companion/webview): prefer ACP session id for export * feat(vscode-ide-companion): support /export slash command Add nested /export completion and ACP command availability for the VS Code companion. Reuse the shared export flow, write to the default path, and show clickable export results in chat. * fix(export): align slash command messaging Restore the CLI export description to the existing wording. Keep the VS Code companion error message consistent with the required /export subcommands. * fix(webui): support explicit markdown file links Handle local markdown file links in assistant messages even when automatic file-link detection is disabled. Normalize encoded paths and line fragments so exported files can be opened from the VS Code webview. * test(vscode-ide-companion): make export path assertion cross-platform * fix(vscode-ide-companion): use public session export entrypoint * fix(cli): replay standalone ESC after early capture * fix(vscode-ide-companion): resolve rebase artifacts and vitest export alias Remove duplicate AvailableCommand import caused by merge, and add vitest resolve alias for @qwen-code/qwen-code/export so the session export service tests can resolve the CLI export module from source. * fix(cli): fix getAvailableCommands test mock to use getCommandsForMode The test mock was only setting up getCommands but getAvailableCommands calls getCommandsForMode. Add getCommandsForMode to the mock and set up test data on it instead. * fix(vscode-ide-companion): fix export file link click and add save dialog - Fix file:/// URI handling in MarkdownRenderer: normalizeExplicitFileLink now strips the file:// scheme before checking isAbsolutePath, so exported file links are properly recognized and clickable - Replace direct cwd file write with vscode.window.showSaveDialog() so users can choose the export destination and filename - Handle cancelled save dialog gracefully (return null, skip success message) * fix(webui): scope file link handler to file:// URIs only, fix # in filenames - normalizeExplicitFileLink now returns early for file:// URIs without splitting on #, since vscode.Uri.file() encodes # as %23 in the path. This prevents filenames containing # from being truncated after decode. - Explicit-link click handler now only fires for file:// URI hrefs, not arbitrary relative paths. This prevents model-generated markdown links from bypassing enableFileLinks=false and opening arbitrary files. - Remove unused KNOWN_FILE_EXTENSIONS constant. * fix(vscode-ide-companion): update export tests for save dialog, fix stale JSDoc - Add showSaveDialog mock to sessionExportService.test.ts - Update existing test to verify save dialog is called with correct args - Add test for cancelled save dialog returning null - Fix JSDoc that incorrectly claimed fallback-to-cwd behavior |
||
|
|
93cbad24b1
|
fix(core): preserve reasoning_content during session resume and active sessions (GH#3579) (#3590)
* fix(core): preserve reasoning_content during session resume and active sessions (GH#3579) * chore(core): remove dead thinkingThresholdMinutes config after latch removal (GH#3579) |
||
|
|
c87d2798bd
|
fix(cli): dispatch queued slash commands through the slash path (#3523)
* fix(cli): dispatch queued slash commands through the slash path When the agent was responding and the user queued a message, the drain path joined all queued messages with `\n\n` and submitted them as one prompt. Any slash command in that blob (e.g. `/model`) no longer started with `/`, so it was sent to the model as plain text instead of opening the command's dialog. The mid-turn tool-result drain had the same problem: it drained the entire queue into the tool-result payload, so a slash command queued during tool execution was injected as context for the model rather than executed as a command. Queue draining now splits into segments — consecutive plain-text messages are still batched into one submission, while slash commands are submitted alone so their `/` prefix survives. The mid-turn drain only takes leading plain-text messages and leaves slash commands queued for the normal idle drain. The idle drain is gated on open dialogs so a queued `/model` does not cause the following queued prompt to be sent to the model while the picker is still open, and a re-entry lock plus a nonce close the race between state commits and the async dialog-open. * fix(cli): defer queued slash commands until idle * fix(cli): drop queued messages on cancel instead of auto-submitting Cancel's contract is now "abort and redirect" in both cancel paths: restore the most recent queued segment into the buffer for editing and drop the rest, so forgotten follow-ups cannot auto-submit once the turn settles. Previously the non-tool path left queued plain-text segments in place for the idle drain to fire, and the tool-executing path cleared only the buffer — both surprised users with belated message dispatches after they had already cancelled. * refactor(cli): batch plain prompts in idle drain Idle drain now runs in two phases: drain all plain-text prompts into one turn (drainQueue), then pop slash commands one-by-one (popNextSegment). Mirrors the mid-turn behavior so queue handling is consistent across mid-turn and idle contexts. popAllMessages now drains the entire queue joined with \n\n for Ctrl+C cancel and ESC/Up edit-restore. Drop the unused options parameter from useMessageQueue and the extractFirstSegment helper. --------- Co-authored-by: 愚远 <zhenxing.tzx@alibaba-inc.com> |
||
|
|
2aad7c0617
|
fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output (#3544)
* fix(cli): disable Kitty keyboard protocol on SIGINT to prevent garbled 9;5u output
When a Kitty-capable terminal (iTerm2, Kitty, WezTerm) is used, the CLI
enables the Kitty keyboard protocol at startup via ESC[>1u. On exit, the
protocol must be disabled with ESC[<u to restore the terminal's default
key encoding. Failing to do so leaves the terminal in Kitty mode: any
subsequent Ctrl+C press is encoded as ESC[99;5u, and since the shell does
not understand this sequence, it echoes the trailing '9;5u' as garbled
text.
Root cause: kittyProtocolDetector registered cleanup handlers for 'exit'
and 'SIGTERM', but omitted SIGINT. A process terminated via SIGINT (e.g.
kill -INT <pid>, a parent process sending SIGINT, or certain process
managers) would exit without disabling the protocol.
Fix:
1. Add process.on('SIGINT', disableProtocol) alongside the existing
'exit' and 'SIGTERM' handlers in kittyProtocolDetector.ts.
2. Export a new disableKittyProtocol() function for explicit call sites.
3. Call disableKittyProtocol() in the registerCleanup callback in
gemini.tsx before instance.unmount(), so the disable sequence is
written while stdout is fully operational regardless of exit path.
Fixes #3528
* fix(test): add disableKittyProtocol to kittyProtocolDetector mock
|
||
|
|
d75c13aae0
|
fix(cli): run ACP Agent tool calls concurrently (#2516) (#3463)
* fix(cli): run ACP Agent tool calls concurrently (#2516)
When the model returns multiple Agent tool calls in a single turn, the
ACP Session previously executed them sequentially in a plain for-loop,
multiplying latency by the number of sub-agents spawned.
Mirror the partition logic in coreToolScheduler.partitionToolCalls:
consecutive Agent calls form a parallel batch (safe because sub-agents
have no shared mutable state); any other tool forms its own sequential
batch so the model's implicit ordering is preserved. Response-part
ordering still matches the original functionCalls order.
Add a focused test that uses controllable deferred executes to prove
both Agent calls start before either resolves, and that the fed-back
functionResponse ordering is stable regardless of resolution order.
* Address PR #3463 review: bound concurrency + robust test timing
Two issues raised by the /review bot:
1. The raw Promise.all fan-out bypassed the bounded-concurrency guard
that coreToolScheduler applies via QWEN_CODE_MAX_TOOL_CONCURRENCY.
Replaced with an inline runBounded helper that mirrors core's
runConcurrently (Promise.race on a bounded executing set, default
cap 10), keeping in-order result collection.
2. The concurrency test used a 10-iteration microtask yield loop before
asserting both execute() spies had been invoked. That's fragile —
runTool's pre-execute path (build → getDefaultPermission →
evaluatePermissionRules → permission branch → PreToolUseHook) has
more await boundaries than 10 ticks guarantees, and the CI run
reported call-a still at 0 invocations at the assertion point.
Reworked the test to wait on an explicit `called` deferred that
resolves *inside* the execute() mock body. Under sequential
behaviour only one `called` would ever fire → `Promise.all([called-a,
called-b])` deadlocks → vitest's per-test timeout surfaces the
regression. Under the fix both fire before either result resolves.
* fix(acp): degrade gracefully when AgentTool invocation has no eventEmitter
The concurrency test for #2516 timed out on CI with "Test timed out in
5000ms" after the `await Promise.all([called-a, called-b])` rewrite in
the previous review-fix commit. The 5000ms wait was the symptom; the
root cause is that neither `execute()` was ever being called.
runTool's AgentTool branch was guarded with `'eventEmitter' in invocation`,
which is a *key-presence* check. The test mock provides
`{ eventEmitter: undefined, ... }` — the key exists (value undefined),
the branch is entered, and `SubAgentTracker.setup` immediately throws
inside `eventEmitter.on(...)`. The try/catch in runTool swallows the
throw and returns an error response, so `invocation.execute()` never
runs, `called[id].resolve()` never fires, and the test deadlocks.
The earlier review commit (
|
||
|
|
97926a07fe
|
fix(acp): support SSE and HTTP MCP servers in ACP mode (#3574)
In ACP mode, the Mcp server list sent by the IDE client can include
SSE (type: "sse") and HTTP (type: "http") transports, but the previous
implementation only handled stdio servers via toStdioServer(). Non-stdio
servers were silently skipped (continue), so any SSE/HTTP-configured
MCP server would never be registered.
Changes:
- Add toSseServer() helper: detects type=="sse" servers and maps them
to MCPServerConfig(url=..., headers=...)
- Add toHttpServer() helper: detects type=="http" servers and maps them
to MCPServerConfig(httpUrl=..., headers=...)
- Refactor newSessionConfig() loop to handle all three transport types
- Declare mcpCapabilities: { sse: true, http: true } in agentCapabilities
so IDE clients know this agent supports these transports without needing
a transparent proxy
- Export the three helper functions for unit testing
Tests:
- Unit tests for toStdioServer / toSseServer / toHttpServer helpers
(type discrimination, mutual exclusion)
- Integration-style tests for QwenAgent.initialize() mcpCapabilities
- Integration-style tests for newSession() with SSE/HTTP MCP servers,
verifying MCPServerConfig is constructed with the correct arguments
(url vs httpUrl, headers passthrough, empty-headers → undefined)
Fixes #3472
|
||
|
|
5556699e43
|
fix(cli): promote resubmitted history prompt to most recent (#3531)
Selecting an older entry from input history via the arrow keys and pressing Enter now moves that entry to the most recent position, so the next Up press surfaces it first. Previously two bugs combined to keep stale copies in place: the history-navigation index was not reset on submit, and deduplication only collapsed consecutive repeats, leaving non-consecutive duplicates intact. |
||
|
|
aeeb2976d6
|
feat(web-search): remove built-in web_search tool, replace with MCP-based approach (#3502)
* feat(web-search): add GLM (ZhipuAI) web search provider - Add GlmProvider class implementing BaseWebSearchProvider using the ZhipuAI Web Search API (https://open.bigmodel.cn/api/paas/v4/web_search) - Support multiple search engines: search_std, search_pro, search_pro_sogou, search_pro_quark - Support optional config: maxResults, searchIntent, searchRecencyFilter, contentSize, searchDomainFilter - Truncate query to 70 characters per API limit - Register 'glm' in the provider discriminated union (types.ts) and createProvider() switch (index.ts) - Add GlmProviderConfig to settingsSchema, ConfigParams, and Config class - Add --glm-api-key CLI flag and GLM_API_KEY env var support in webSearch.ts - Forward GLM_API_KEY in sandbox environment - Update provider priority list: Tavily > Google > GLM > DashScope - Add 17 unit tests for GlmProvider and 4 integration tests in index.test.ts - Update docs/developers/tools/web-search.md with GLM configuration, env vars, CLI args, pricing, and corrected DashScope billing info - Fix stale OAuth/free-tier references in web-search.md Closes #3496 * docs(web-search): fix DashScope note and add GLM server-side limitations * fix(web-search): make DashScope provider work with standard API key, remove qwen-oauth dependency - DashScopeProvider.isAvailable() now checks config.apiKey instead of authType - Remove OAuth credential file reading and resource_url requirement - Use standard DashScope endpoint: dashscope.aliyuncs.com/api/v1/indices/plugin/web_search - Read DASHSCOPE_API_KEY env var and --dashscope-api-key CLI flag - Forward DASHSCOPE_API_KEY into sandbox environment - Update integration test to detect DASHSCOPE_API_KEY - Update docs to reflect new API key based configuration * feat(web-search): remove built-in web search tool The web_search tool and all related provider implementations are removed. Web search functionality will be provided via MCP integrations instead, which is the direction the broader agent ecosystem is moving. Removed: - packages/core/src/tools/web-search/ (entire directory) - packages/cli/src/config/webSearch.ts - integration-tests/cli/web_search.test.ts - ToolNames.WEB_SEARCH, ToolErrorCode.WEB_SEARCH_FAILED - webSearch config in ConfigParams, Config class, settingsSchema - CLI options: --tavily-api-key, --google-api-key, --google-search-engine-id, --glm-api-key, --dashscope-api-key, --web-search-default - Sandbox env forwarding for TAVILY/GLM/DASHSCOPE/GOOGLE search keys - web_search from rule-parser, permission-manager, speculation gate, microcompact tool set, and builtin-agents tool list * fix: remove websearch reference * docs: remove websearch tool * docs: add break change guide * fix review |
||
|
|
3182500835
|
fix(cli): remove residual blank lines after MCP init completes (#3509)
* fix(cli): remove residual blank lines after MCP init completes (#3095) ConfigInitDisplay rendered <Box marginTop={1}> plus a content line, so the live area grew by 2 rows during startup. When initialization finished and the component unmounted, Ink shrank the live area but the rows it had already committed to the terminal scrollback cannot be reclaimed, leaving a visible gap above the input. Move the MCP init status into the Footer's left-bottom status slot (always mounted, fixed height) so the live area height stays constant across the init → ready transition. The status participates in the existing priority chain: ctrlC / ctrlD / escape / vim / shell / autoAccept / configInit / hint. * fix(cli): suppress MCP init message when custom status line is active Audit follow-up. Previously the configInit branch preceded the suppressHint branch in the footer's left-bottom priority chain. With a custom status line configured, <Text>{null}</Text> collapses to zero rows in Ink, so the footer's bottom row went from 1 row during init to 0 rows after — a 1-row height oscillation that reintroduces the same scrollback-residue symptom the original fix eliminated in the default case. Swap the order so suppressHint short-circuits to null first: the init message now shares the hint's suppression rule, keeping the footer's height constant in every configuration. Also: - Gate the hook's return on isConfigInitialized directly instead of letting the effect clear state, avoiding a one-frame flash where the stale "Initializing..." message leaks through on the first render after init completes. - Cover the new behavior with three Footer tests, including a regression test for the custom-status-line case. * fix(cli): show MCP init progress even under a custom status line Reverting a UX trade-off introduced in the previous commit. That change suppressed the init message whenever a custom status line was active, arguing that <Text>{null}</Text> collapses to zero rows in Ink and any non-zero init row would re-create a one-row shrink on completion. Zero shrink was the wrong goal. Hiding init progress from users who have configured a status line is a real usability loss — the status line does not surface MCP connection state, so those users now see no feedback during startup. A one-time, one-line shrink on init completion is a far smaller regression than the original two-row scrollback residue this PR was created to fix, and strictly better than the silent alternative. Keep the init message in the left-bottom slot and let it sit above suppressHint in the priority chain. Update the regression test so that it pins the new behavior (init is visible with or without a status line) and prevents the suppression from being reintroduced. * fix(cli): keep MCP init progress visible in screen-reader mode Footer is gated behind !isScreenReaderEnabled, so moving the init message inside Footer silenced it for screen-reader users. Render the same message as a plain Text node in Composer when the screen reader is active — screen-reader users don't suffer from the live-area residual row issue that motivated the original move, so an independent node is safe for them. * refactor(cli): drop duplicated screen-reader init path and show progress under YOLO - ScreenReaderAppLayout already mounts <Footer /> directly, so the separate <Text> branch in Composer was producing a duplicated 'Connecting to MCP servers...' line in screen-reader mode. Remove it. - Move configInitMessage ahead of AutoAcceptIndicator in the footer's priority chain so users launched with YOLO / auto-accept-edits still see the ~1s startup progress; the approval-mode indicator takes over as soon as init finishes. - Add unit tests for useConfigInitMessage covering the idle, progress, reset, and unsubscribe paths. |
||
|
|
4e0a37549d
|
fix(i18n): sync mismatched keys between en.js and zh.js (#3534)
Some checks are pending
E2E Tests / E2E Test - macOS (push) Waiting to run
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
* fix(i18n): sync mismatched keys between en.js and zh.js (#3503) Add 4 keys missing from en.js that are actively used in source code, add 5 missing Chinese translations to zh.js, integrate check-i18n into CI to prevent future drift, and skip JSON file write in CI to avoid dirtying the working tree. --- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
d36f12c4c4
|
feat(session): auto-title sessions via fast model, add /rename --auto (#3540)
* feat(session): auto-title sessions via fast model, add /rename --auto The /rename work in #3093 generates kebab-case titles only when the user explicitly runs `/rename` with no args; until they do, the session picker shows the first user prompt (often truncated or misleading). This change adds a sentence-case auto-title that fires once per session after the first assistant turn, using the configured fast model. New service: `packages/core/src/services/sessionTitle.ts` — `tryGenerateSessionTitle(config, signal)` returns a discriminated outcome (`{ok: true, title, modelUsed}` | `{ok: false, reason}`) so callers can either handle failures generically or map reasons to actionable messages. Prompt shape: 3-7 words, sentence case, good/bad examples including a CJK row, JSON schema enforced via `baseLlmClient.generateJson`. `maxAttempts: 1` — titles are cosmetic metadata and shouldn't fight rate limits. Trigger point: `ChatRecordingService.maybeTriggerAutoTitle` runs after `recordAssistantTurn`. Fire-and-forget promise, guarded by: - `currentCustomTitle` — don't overwrite any existing title. - `autoTitleController` doubles as in-flight flag; a second turn while the first is still pending is a no-op. - `autoTitleAttempts` cap of 3 — the first assistant turn may be a pure tool-call with no user-visible text; retry for a handful of turns until a title lands. Cap bounds total waste. - `!config.isInteractive()` — headless CLI (`qwen -p`, CI) never auto- titles; spending fast-model tokens on a one-shot session is waste. - `autoTitleDisabledByEnv()` — `QWEN_DISABLE_AUTO_TITLE=1` opt-out. - `config.getFastModel()` falsy — skip entirely rather than falling back to the main model; auto-titling on main-model tokens is too expensive to be silent. Persistence: `CustomTitleRecordPayload` grows a `titleSource: 'auto' | 'manual'` field. Absent on pre-change records (treated as `undefined` → manual, safe default so a user's pre-upgrade `/rename` is never silently reclassified). `SessionPicker` renders `titleSource === 'auto'` titles in dim (secondary) color; manual stays full contrast. On resume, the persisted source is rehydrated into `currentTitleSource` — without this, finalize's re-append would rewrite an auto title as manual on every resume cycle. Cross-process manual-rename guard: when two CLI tabs target the same JSONL, in-memory state can diverge. Before writing an auto record, the IIFE re-reads the file via `sessionService.getSessionTitleInfo`. If a `/rename` from another process landed as manual, bail and sync local state — never clobber a deliberately-chosen manual title with a model guess. Cost is one 64KB tail read per successful generation. `finalize()` aborts the in-flight controller before re-appending the title record. Session switch / shutdown doesn't have to wait on a slow fast-model call. New user-facing command: `/rename --auto` regenerates via the same generator — explicit user trigger, overwrites whatever's there (manual or auto) because the user asked. Errors route through `autoFailureMessage(reason)` so `empty_history`, `model_error`, `aborted`, etc. each get actionable guidance rather than a generic "could not generate". `/rename -- --literal-name` is the sentinel for titles that start with `--`; unknown `--flag` tokens error with a hint pointing at the sentinel. Existing `/rename <name>` and bare `/rename` (kebab-case via existing path) are unchanged, except the kebab path now prefers fast model when available and runs its output through `stripTerminalControlSequences` (same ANSI/OSC-8 hardening as the sentence-case path). New shared util: `packages/core/src/utils/terminalSafe.ts` — `stripTerminalControlSequences(s)` strips OSC (\x1b]...\x07|\x1b\\), CSI (\x1b[...[a-zA-Z]), SS2/SS3 leaders, and C0/C1/DEL as a backstop. A model-returned `\x1b[2J` or OSC-8 hyperlink escape would otherwise execute on every SessionPicker render; both sentence-case and kebab paths now route titles through the helper before they reach the JSONL or the UI. Tail-read extractor: `extractLastJsonStringFields(text, primaryKey, otherKeys, lineContains)` reads multiple fields from the same matching line in a single pass. Two separate tail scans could return a mismatched pair (primary from a newer record, secondary from an older one with only the primary set); the new helper guarantees the pair is atomic. Validates a proper closing quote on the primary value so a crash-truncated trailing record can't win the latest-match race. `readLastJsonStringFieldsSync` is its file-reading wrapper — same tail-window fast path and full-file fallback as the single-field version, plus a `MAX_FULL_SCAN_BYTES = 64MB` cap so a corrupt multi-GB session file can't freeze the picker. Session reads now open with `O_NOFOLLOW` (falls back to plain RDONLY on Windows where the constant isn't exposed) — defense in depth against a symlink planted in `~/.qwen/projects/<proj>/chats/`. Character handling: `flattenToTail` on the LLM prompt drops a dangling low surrogate after `slice(-1000)` — otherwise a CJK supplementary char or emoji cut mid-pair produces invalid UTF-16 that some providers 400. `sanitizeTitle` applies the same surrogate scrub after max-length trim, and strips paired CJK brackets (`「」 『』 【】 〈〉 《》`) as whole units so a `【Draft】 Fix login` doesn't leave a dangling `】` after leading-char strip. `lineContains` in the title reader is tightened from the loose substring `'custom_title'` to `'"subtype":"custom_title"'` so user text containing the literal `custom_title` can't shadow a real record. Tests: 46 new unit tests across - `sessionTitle.test.ts` (22): success/all-failure-reasons, tool-call filter, tail-slice, surrogate scrub, ANSI/OSC-8 strip, CJK brackets. - `chatRecordingService.autoTitle.test.ts` (15): trigger/skip matrix, in-flight guard, abort propagation on finalize, manual/auto/legacy resume symmetry, cross-process race, env opt-out, retry-after- transient. - `sessionStorageUtils.test.ts` (13): single-pass extractor, straddle boundary, truncated trailing record, lineContains, multi-field atom. - `renameCommand.test.ts` (8): `--auto` success, all reasons, sentinel, unknown-flag hint, positional rejection, manual/SessionService fallbacks. * docs(session): design doc for auto session titles Matches the session-recap design doc shape (Overview / Triggers / Architecture / Prompt Design / History Filtering / Persistence / Concurrency / Configuration / Observability / Out of Scope) and adds a Security Hardening section unique to the title path — titles render directly in the picker and persist in user-readable JSONL, so LLM-returned control sequences are an attack surface the recap path doesn't have. Captures decisions a code-only reader has to reverse-engineer: - Why `maxAttempts: 1` (best-effort cosmetic metadata; no retry loop). - Why `autoTitleAttempts` cap is 3 (first turn can be pure tool-call). - Why the auto trigger does NOT fall back to the main model but session-recap does (auto-title fires on every turn; silently charging main-model tokens is a bill surprise). - Why `titleSource: undefined` stays unwritten on legacy records (no rewrite risks silently reclassifying user intent). - Why the cross-process re-read sits between the LLM await and the append (manual wins at both in-process and on-disk layers). - Why `finalize()`'s abort tolerates a controller swap (in-flight identity check). - Why JSON-schema function calling instead of tag extraction (avoid reasoning preamble bleed; cross-provider reliability). Placed at docs/design/session-title/ alongside session-recap, compact-mode, fork-subagent, and other per-feature design docs. No sidebar index update required — the design folder is unindexed. * test(rename): pin model choice in bare /rename kebab path Addresses reviewer feedback: the bare `/rename` model selection (`config.getFastModel() ?? config.getModel()`) had no test pinning it either way. Previous tests mocked `getHistory: []`, which exits the function before the model is ever chosen, so a silent regression to either direction (always-main or always-fast) would pass CI. Two explicit cases now: - fastModel set → `generateContent` called with `model: 'qwen-turbo'`. - fastModel unset → `generateContent` called with `model: 'main-model'`. The tests intentionally mock a non-empty history so the kebab path reaches the generateContent call site instead of bailing on empty input. |
||
|
|
9010c09123
|
chore: bump version to 0.15.1 (#3541)
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
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> |
||
|
|
78037d996b
|
fix(cli): stabilize resume callback deps (#3533) | ||
|
|
69da115dcf
|
feat(cli): combine elapsed + timeout in shell time indicator (#3512)
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): combine elapsed + timeout in shell time indicator Render shell tools that have an explicit timeout as `(elapsed · timeout N)` inline with the Running… status from t=0, instead of splitting the information across the right-aligned elapsed indicator and the ShellStatsBar row. - formatters: add a `hideTrailingZeros` option so whole seconds render as `5s` rather than `5.0s` while fractional values like `5.5s` stay intact - ToolElapsedTime: accept optional `timeoutMs`; when set, skip the 3s quiet threshold and render the combined `(elapsed · timeout N)` label - ToolMessage: extract `timeoutMs` from AnsiOutputDisplay and feed it to ToolElapsedTime - ShellStatsBar: drop its `timeoutMs` field (now inline); keeps `+N lines` and memory usage only - Unify both modes on `formatDuration` so hour-range output is consistent (`1h 2m 6s` across timeout and no-timeout paths) * feat(cli): thread shell timeoutMs through compact tool group display The combined `(elapsed · timeout N)` format introduced in the previous commit was only wired through the expanded ToolMessage path. Compact tool groups kept rendering ToolElapsedTime without timeoutMs, so shell tools displayed in compact mode silently dropped the timeout budget. - CompactToolGroupDisplay: add getShellTimeoutMs() to pull timeoutMs off the active tool's AnsiOutputDisplay result (same shape used by ToolMessage) and feed it to ToolElapsedTime - add CompactToolGroupDisplay.test.tsx covering the three paths: ansi display with timeoutMs, ansi display without timeoutMs, and non-ansi resultDisplay (string) |
||
|
|
f2fac208ff
|
chore(release): bump version to 0.15.0 (#3526)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Upgrade all package versions from 0.14.5 to 0.15.0 across the monorepo, including package-lock.json and sandbox image references. |
||
|
|
2710bdec0d
|
feat(cli): Phase 2 — slash command multi-mode expansion, ACP fixes, and UX improvements (#3377)
* 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
* feat(cli): Phase 2 slash command expansion + ACP fixes + UX improvements
Phase 2.1 - Command mode expansion:
- Extend 13 built-in commands to support non_interactive/acp modes
- A class: export, plan, statusline - supportedModes only
- A+ class: language, copy, restore - add non-interactive branches
- A' class: model, approvalMode - handle dialog paths in non-interactive
- B class: about, stats, insight, docs, clear - full non-interactive branches
- context: format output as readable Markdown instead of raw JSON
- export: use HTML as default format when no subcommand given
Phase 2.2 - SkillTool integration:
- SkillTool now consumes CommandService.getModelInvocableCommands()
Phase 2.3 - Mid-input slash ghost text:
- Replace mid-input dropdown completion with inline ghost text
- Match Claude Code behavior: gray dimmed completion hint in input box
- Tab accepts the ghost text completion
- Add findMidInputSlashCommand() and getBestSlashCommandMatch() utilities
ACP session bug fixes:
- Fix executionMode undefined in interactive mode (slashCommandProcessor)
- Fix slash command output not visible in Zed (use emitAgentMessage)
- Fix newline rendering in Zed (Markdown hard line-break)
- Fix history replay merging consecutive user messages (recordSlashCommand)
- Fix /clear not clearing model context (dynamic chat reference)
* feat: inline complete only for modelInvocable
* 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
* fix mcp prompt in skill manager
* revert pr#3345
* fix test ci
* feat(cli): adapt /insight for non_interactive mode with message return
- non_interactive: run generateStaticInsight() synchronously with no-op
progress callback, return { type: 'message' } with output path
- acp: keep existing stream_messages path with progress streaming
- interactive: unchanged
Add tests for non_interactive success and error paths.
Update phase2-technical-design.md and roadmap.md to reflect the
three-way mode split and clarify that MCP prompts do not need
modelInvocable (they are called via native MCP tool call mechanism).
* fix(cli): ghost text only shown when cursor is at end of slash token
Use strict equality (!==) instead of > in findMidInputSlashCommand so that
ghost text is only computed and Tab-accepted when the cursor sits exactly at
the trailing edge of the partial command token.
Previously, with the cursor inside an already-typed token (e.g. /re|view),
the ghost text suffix would still be shown and pressing Tab would insert it
at the cursor position, producing a duplicated tail. Using strict equality
makes ghost text disappear as soon as the cursor moves inside the token.
Add unit tests for findMidInputSlashCommand covering cursor-at-end,
cursor-inside-token, cursor-past-token, start-of-line, and
no-space-before-slash cases.
* fix(cli): support /model <model-id> in non-interactive and ACP modes
Previously, /model <model-id> (without --fast) fell through to the
non-interactive branch that only returned the current model info and
incorrectly told users to use --fast. Now:
- /model <model-id> → sets the main model via settings + config.setModel()
- /model → shows current model with correct usage hint
- /model --fast <id> → unchanged (sets fast model)
Fixes the inconsistency flagged in PR review: the help text said to use
'/model <model-id>' but the command returned a dialog action which is
unsupported in non-interactive mode.
* fix(cli): declare supportedModes on doctorCommand to enable non-interactive and ACP
The command's action already had non-interactive handling (returns a JSON
message with check results), but without supportedModes declared the
BUILT_IN fallback restricted it to interactive-only so it was never
registered in non_interactive or acp sessions.
* feat(skills): add SkillCommandLoader for user/project/extension skills as slash commands
- New SkillCommandLoader loads user, project, and extension level SKILL.md
files as slash commands (previously only bundled skills were slash-invocable)
- Extension skills follow plugin-command rules: modelInvocable only when
description or whenToUse is present
- User/project skills are always modelInvocable (matching bundled behavior)
- skill-manager now injects extensionName when loading extension-level skills
- Add when_to_use and disable-model-invocation frontmatter support to SKILL.md
and .md command files (SkillConfig, markdown-command-parser, command-factory,
BundledSkillLoader, FileCommandLoader)
- SkillTool filters out skills with disableModelInvocation and includes
whenToUse in the skill description shown to the model
- 16 unit tests for SkillCommandLoader covering all cases
* docs: update phase2 design doc to reflect final decisions on plan/statusline/copy/restore
These four commands are intentionally kept as interactive-only by design:
- /plan and /statusline: tightly coupled with interactive multi-turn UI
- /copy and /restore: clipboard and snapshot restore are inherently interactive
Update design doc classification table, section 4.2, 4.3, 5.2, 5.3,
file change summary, test requirements, behavior analysis table,
and implementation batch descriptions to reflect this decision.
* feat(cli): re-implement slashCommands.disabled denylist based on current refactored code
Adapts the feature originally introduced in pr#3445 to the current
CommandService / Phase-2 refactored code.
Sources (merged, de-duplicated, case-insensitive):
- settings key slashCommands.disabled (string[], UNION merge)
- --disabled-slash-commands CLI flag (comma-separated or repeated)
- QWEN_DISABLED_SLASH_COMMANDS environment variable
Enforcement points:
- CommandService.create() accepts optional disabledNames: ReadonlySet<string>
and removes matching commands post-rename, so disabled commands never appear
in autocomplete, mid-input ghost text, or model-invocable commands list.
- slashCommandProcessor (interactive TUI) passes the denylist to
CommandService.create so disabled commands are absent from dropdown/ghost text.
- nonInteractiveCliCommands.handleSlashCommand() keeps allCommands unfiltered
to distinguish disabled vs unknown; disabled commands return unsupported with
a "disabled by the current configuration" reason (not no_command).
- getAvailableCommands() (ACP) passes the denylist to CommandService.create.
Config plumbing:
- core/Config: ConfigParameters.disabledSlashCommands + getDisabledSlashCommands()
- cli/config: CliArgs.disabledSlashCommands + yargs option + loadCliConfig merge
- settingsSchema: slashCommands.disabled (MergeStrategy.UNION)
- settings.schema.json: regenerated
Tests: 28 pass (CommandService x4, nonInteractiveCliCommands x3 new cases)
* feat(cli): complete slashCommands.disabled coverage from pr#3445
Fill in the three items that were missing from the initial re-implementation:
- packages/cli/src/config/settings.test.ts: add UNION-merge test for
slashCommands.disabled across user and workspace scopes
- packages/cli/src/nonInteractiveCli.test.ts: add getDisabledSlashCommands
mock to the shared mockConfig fixture
- docs/users/configuration/settings.md: add slashCommands section (table +
example + note) and --disabled-slash-commands row in the CLI args table
* fix(cli): match disabled slash commands by alias as well as primary name
The denylist previously only checked cmd.name (the primary/canonical name),
so disabling a command by its alias (e.g. 'about' for the 'status' command)
had no effect. Fix both CommandService.create() and the isDisabled() helper
in nonInteractiveCliCommands.ts to also check altNames.
Also improve the user-facing error message to show the token the user actually
typed (e.g. /about) instead of always showing the primary name (/status).
|
||
|
|
58cdf101ba
|
feat(cli): auto-detect terminal theme ('auto' or unset) (#3460)
* feat(cli): add terminal theme auto-detection when ui.theme is 'auto' Detect terminal dark/light preference at startup using macOS system appearance (AppleInterfaceStyle) and COLORFGBG env variable fallback, then resolve to Qwen Dark or Qwen Light accordingly. Adds 'Auto' option to the /theme dialog. Closes #2998 * fix: address audit issues in terminal theme detection - Fix ThemeDialog preview: use getActiveTheme() when 'auto' is highlighted so the preview shows the actual detected theme instead of always falling back to Qwen Dark. - Swap detection order: check COLORFGBG (terminal-specific) before macOS system appearance (system-wide) since the terminal may use a different theme than the OS. - Fix core/theme.test.ts mock to export AUTO_THEME_NAME and add test case verifying 'auto' bypasses validation. * feat(cli): add OSC 11 background color query for theme detection Send ESC]11;?BEL to the terminal at startup to read the actual background RGB value, then decide dark/light via ITU-R BT.709 luminance. This is the most universal detection method and covers Linux terminals (GNOME Terminal, Windows Terminal, etc.) that do not set COLORFGBG. Async detection (OSC 11 → COLORFGBG → macOS → dark) is used at startup; the sync path (COLORFGBG → macOS → dark) remains for the /theme dialog live-preview to avoid ~200ms latency per highlight. * fix: optimize async detection order and improve comments - Check COLORFGBG first in the async path to avoid a 200ms OSC 11 timeout on terminals that already set COLORFGBG but lack OSC 11. - Fix misleading comment about stdin flowing mode vs raw mode. * fix(cli): defer auto theme detection past sandbox entry - Move resolveAutoThemeAsync() to after the sandbox-check gate so the ~200ms OSC 11 probe does not block a process that is about to exec into the sandbox child (which reruns the same detection). - Register missing i18n keys 'Auto (detect terminal theme)' and 'Auto' across all 7 locales; previously non-English users fell back to the English keys. - Simplify resolveAutoThemeAsync to return Promise<void> (the caller never checked the previous always-true boolean). * feat(cli): auto-detect theme when ui.theme is unset An unset ui.theme now behaves the same as 'auto' — the async OSC 11 / COLORFGBG / macOS probe runs at startup and resolves to Qwen Dark or Qwen Light. Fresh installs no longer hard-code Qwen Dark. The /theme dialog also highlights the "Auto" row when ui.theme is undefined, so the selection reflects the effective resolution. * fix(cli): do not run OSC 11 probe when ui.theme is unset Fresh startups were showing kitty-protocol response bytes (e.g. [?0u[?62c) inside the input box. The OSC 11 probe added for the unset-theme path flips stdin raw mode and pauses the stream, and that state dance interleaves with kitty protocol detection on some terminals so the kitty responses leak past the early-input-capture filter and land in the TUI input. Fall back to the synchronous detector (COLORFGBG + macOS) when the user has no theme configured. Explicit 'auto' still runs the OSC 11 probe since the user has opted in. * fix(cli): run OSC 11 probe inside the early-capture window Previous fix restricted the OSC 11 probe to explicit 'auto', leaving fresh installs without terminal detection — not acceptable. The real problem was that the probe managed its own stdin raw mode and pause cycle before early input capture was attached, so kitty protocol response bytes arriving during the gap slipped past the filter and landed in the TUI input. - Make detectOsc11Theme stdin-state-agnostic: it no longer flips raw mode or pauses the stream; it just attaches a listener, sends the query, and removes the listener on response or timeout. - Defer the async probe in gemini.tsx until after startEarlyInputCapture (and kitty detection kickoff) inside the interactive block. The existing filter in startEarlyInputCapture absorbs the OSC 11 response bytes alongside our handler, so nothing can leak into the TUI input. - Both unset theme and explicit 'auto' now run the async probe. * fix(cli): sync theme baseline for non-interactive and pre-render UI The previous refactor only resolved 'auto'/unset themes inside the interactive startup block. That dropped detection for non-interactive runs and left any pre-render UI (the --resume session picker) drawing with the default Qwen Dark palette even on light terminals. Set a synchronous baseline (COLORFGBG + macOS) right after loading custom themes so the theme is already correct when those paths run; the interactive block still refines with an OSC 11 probe when possible. * fix(cli): cache async auto-detect so /theme Auto stays consistent /theme's live preview calls setActiveTheme('auto'), which runs the synchronous detector (COLORFGBG + macOS only). On terminals whose light/dark state is only visible to OSC 11 (e.g. GNOME Terminal), the sync path disagrees with the async probe done at startup — so picking Auto once showed the correct preview, but switching away and picking Auto again flipped the preview to the wrong theme. Cache the result from resolveAutoThemeAsync and prefer it in the sync path; fall back to live sync detection only when no async result is known yet. Added a unit test that locks the regression down. * fix(theme): don't pin macOS detection to Light on generic exec failure detectMacOSTheme previously treated every `defaults read -g AppleInterfaceStyle` failure as Light Mode. Only the "key does not exist" error actually indicates Light — timeouts, missing `defaults`, ENOENT, SIGTERM, etc. are inconclusive and should fall through so the caller can continue its fallback chain instead of locking to Light. Match the "does not exist" marker in the error's stderr or message; return undefined otherwise. Adds tests for the timeout, ENOENT and stderr-only paths. * perf(cli): overlap OSC 11 theme probe with startup work resolveAutoThemeAsync was awaited on the critical path, so an unset or 'auto' ui.theme paid the full OSC 11 timeout (~200 ms) plus the synchronous macOS defaults read before the first paint. The synchronous baseline picked earlier already keeps the theme valid for the non-interactive paths and the pre-render UI, so this await was the only thing forcing render to wait on the probe. Kick the probe off without awaiting alongside detectAndEnableKittyProtocol and drain the resulting promise just before startInteractiveUI. The OSC 11 timeout now overlaps with initializeApp and the warnings collection, the early-capture filter is still active when the response arrives (so no terminal bytes leak into the TUI), and the refined theme is in place by the time the first frame renders. * test(cli): cover OSC 11 probe listener lifecycle Adds regression tests for the listener-leak path that motivated three mid-PR fixes (OSC 11 bytes bleeding into the input box): - happy-path resolves 'dark' from a simulated terminal response and asserts the data listener is removed - timeout path resolves undefined and likewise restores the listener count to baseline - multi-chunk path reassembles a response split across two data events Also resets the module-level `cachedAutoDetection` singleton in the theme-manager beforeEach so the async detection cache cannot leak across tests and make ordering load-bearing. |
||
|
|
b842f27c99
|
fix(cli): inject plan/subagent/arena system reminders in ACP (#1151) (#3479)
* fix(cli): inject plan/subagent/arena system reminders in ACP (#1151) The ACP Session sends user messages via chat.sendMessageStream() directly, bypassing GeminiClient.sendMessageStream() where the CLI/TUI path injects its per-turn system reminders. As a result: - Plan mode is silently inert in ACP: the model never sees the reminder that tells it to avoid edits and call exit_plan_mode, so it tries to run edit tools and triggers the plan-mode block-check only as a fallback. - User-level subagents registered in the workspace are invisible to the model for the same reason. - Arena sessions started via the ACP path lose their session-dir context. Mirror the subagent / plan / arena branches from client.ts:848-878 in a new private helper #buildInitialSystemReminders, and prepend its output to the initial user-query message in #executePrompt as well as the cron path in #executeCronPrompt. The helper intentionally skips the managed auto-memory reminder — that one needs the GeminiClient prefetch pipeline and will be tackled as part of broader middleware alignment. Tests cover plan-mode on/off and non-builtin subagent filtering on the first-turn message fed into chat.sendMessageStream. * test(acp): add ensureTool + getSubagentManager to default mockConfig The system-reminder fix added an unconditional `config.getToolRegistry().ensureTool(ToolNames.AGENT)` call inside `#buildInitialSystemReminders`, which now runs on every `session.prompt()` and every cron fire. The new system-reminder tests stub `ensureTool` via their own `stubEmptySubagents` helper, but the default `mockToolRegistry` at the top of the file still only carries `getTool`. As a result all 13 pre-existing tests that exercise `session.prompt()` blow up with `TypeError: this.config.getToolRegistry(...).ensureTool is not a function`, and the cascaded `StopFailure` assertion fails because the test never reaches the assertion point. Move both `ensureTool` (on the tool registry) and `getSubagentManager` (on the config) into the default beforeEach mocks so every test that calls `session.prompt()` can traverse `#buildInitialSystemReminders` without the caller having to know about it. Defaulting `listSubagents` to an empty array is the harmless zero case — tests that care about subagent reminders already override it. The existing `stubEmptySubagents` helper still works unchanged (its explicit overrides take precedence over the defaults), so the new system-reminder tests in this PR keep expressing intent locally. * test(acp): update afterEach cast to match new mockToolRegistry type The previous commit added `ensureTool` to the `mockToolRegistry` declaration but left the `afterEach` reset casting to the old `{ getTool: ReturnType<typeof vi.fn> }` shape, which TS 5 now rejects under strict mode: error TS2741: Property 'ensureTool' is missing in type '{ getTool: Mock<Procedure>; }' but required in type '{ getTool: Mock<Procedure>; ensureTool: Mock<Procedure>; }'. Use `typeof mockToolRegistry` so the cast tracks the declaration automatically and future additions don't need a second edit. * test(acp): add getApprovalMode to default mockConfig The previous commits wired `#buildInitialSystemReminders` into every `session.prompt()` entry, which also reads `this.config.getApprovalMode()` to decide whether to prepend the plan-mode reminder. The default `mockConfig` never provided `getApprovalMode`, so the five pre-existing prompt-level tests that don't set it locally (passes resolved paths, runtime output dir context, UserPromptSubmit/Stop/StopFailure hooks) crash with `TypeError: this.config.getApprovalMode is not a function` on every platform + node version. Default to `ApprovalMode.DEFAULT` so tests that don't care about approval mode still traverse the helper. The ~10 tests that exercise plan/yolo/auto-edit already reassign `mockConfig.getApprovalMode` locally, and the reassignment wins over the default. |
||
|
|
d71f2fab70
|
feat(cli): cap inline shell output with configurable line limit (#3508)
* feat(cli): cap inline shell output with configurable line limit
Long-running shell commands (npm install, find /, build logs) currently
fill the viewport with the full visible PTY buffer (up to availableHeight,
~24 lines on a typical terminal). The output dominates the screen and
pushes prior context off the top.
This caps inline ANSI shell output to a small window (default 5 lines,
matching Claude Code's ShellProgressMessage). The hidden line count is
already surfaced via the existing `+N lines` indicator in
`ShellStatsBar`, so users still know how much was elided.
The cap applies only when nothing in the existing escape-hatch set is
true:
- `forceShowResult` (errors, !-prefix user-initiated commands,
tools awaiting confirmation, agents pending confirmation)
- `isThisShellFocused` (ctrl+f focus on a running embedded PTY shell)
- `ui.shellOutputMaxLines = 0` (user opt-out)
Also adds a new `ui.shellOutputMaxLines` setting (default 5) so users
can adjust or disable the cap. The SettingsDialog renders it
automatically via the existing `type: 'number'` schema path.
Notes on scope:
- Only the `'ansi'` display branch is capped. `'string'`, `'diff'`,
`'todo'`, `'plan'`, `'task'` renderers are untouched.
- `AnsiOutputDisplay` is only produced by shell tools (`shell.ts`,
`shellCommandProcessor.ts`), so other tool outputs are unaffected.
- The `+N lines` count is bounded by the headless xterm buffer height
(~30 rows) — a pre-existing limitation of the buffer-based stats,
not introduced here.
Tests:
- 4 new ToolMessage tests cover cap default, forceShowResult bypass,
settings disable (cap=0), and custom cap value.
- The existing `MockAnsiOutputText` / `MockShellStatsBar` mocks were
extended to print `availableTerminalHeight` / `displayHeight` so
the cap behavior is asserted at the prop level.
* fix(cli): apply shell output cap to completed string display too
Initial PR caught only the streaming ANSI branch. AI shell tools emit
the final completed result through `shell.ts:returnDisplayMessage =
result.output`, which is a plain string. That string went through
`StringResultRenderer` with the unmodified `availableHeight`, so the
cap was effectively bypassed for the steady-state display the user
actually sees most of the time.
Verified manually in tmux: a `seq 1 30` invocation by the AI now
collapses to "first 26 lines hidden ... 27 28 29 30" instead of
listing all 30 rows. `!`-prefix `seq 1 30` still expands fully via
the existing `isUserInitiated → forceShowResult` bypass.
Changes:
- Detect shell tool by name (matches existing `SHELL_COMMAND_NAME` /
`SHELL_NAME` checks already used in this file)
- Rename `ansiAvailableHeight` → `shellCapHeight` since it now
governs the string branch as well
- Pass `shellCapHeight` to `StringResultRenderer`; the value
falls back to `availableHeight` for non-shell tools so other
tools' string output is unaffected
- Two new tests: shell completed string is capped; non-shell
string is not
- Two existing tests updated to use `name="Shell"` so they actually
exercise the cap path (would previously have passed by accident
since the original code didn't check tool name)
Also picks up the auto-regenerated VSCode IDE companion settings
schema entry for `ui.shellOutputMaxLines`.
* fix(cli): symmetrize ANSI/string row counts and clamp shell cap input
Addresses two non-blocking review observations on #3508.
Off-by-one between paths:
MaxSizedBox reserves one row for its overflow banner when content
exceeds maxHeight (visibleContentHeight = max - 1). The ANSI path
pre-slices to N in AnsiOutputText so MaxSizedBox sees exactly N
rows and renders all N — plus the separate ShellStatsBar line.
The string path passes the raw cap and lets MaxSizedBox handle
overflow, so it shows N-1 content rows + the banner.
Result with cap=5: ANSI showed 5+stats, string showed 4+banner.
Pass shellCapHeight + 1 to StringResultRenderer when capping so
both paths render N visible content rows. Verified in tmux: the
completed Shell tool box now reports `... first 25 lines hidden ...`
followed by lines 26-30 (was 26 + lines 27-30).
Setting validation:
Schema accepts any number; the dialog only rejects NaN. Negatives
silently disabled the cap (only 0 is documented as off) and
fractional values produced fractional slice counts. Added
Math.max(0, Math.floor(value || 0)) at the use site so:
- negatives → 0 → cap disabled (matches the documented opt-out)
- fractions → floor → whole-row cap
- non-numeric (raw settings.json edits) → 0 → cap disabled
Schema-level minimum/integer constraints aren't supported by the
current settings infrastructure (no other number setting uses
them either), so the guard lives at the use site.
Tests:
- Updated string-cap test to assert lines 26-30 visible (catches
the +1 fix; was lines 27-30 before)
- New parameterized test covers -1, 1.5, and a non-numeric value
|
||
|
|
0c423deedf
|
feat(session): add rename, delete, and auto-title generation for session (#3093)
* feat(session): add rename, delete, and auto-title generation for sessions - Add /rename command with LLM auto-title generation when no args provided - Add /delete command to remove sessions from the session picker - Display session name tag embedded in input prompt top border - Restore session name on /resume and --resume <title> CLI flag - Support rename and delete via ACP extMethod for VSCode extension - Add rename/delete UI to WebUI SessionSelector with two-click delete confirmation - Fix parentUuid chain: custom_title records now correctly reference the previous record's UUID, preventing session history from appearing empty after rename - Add SESSION_FILE_PATTERN validation to all SessionService methods that construct file paths from sessionId (defense-in-depth against path traversal) - Fix fd leak in readCustomTitleFromFile with try/finally - Fix --resume <title> exit code (exit 1 when no match found) - Add project ownership checks to VSCode qwenSessionReader delete/rename Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(session): fix broken imports and missing mocks from rename/auto-title feature - Fix renameCommand.ts import path to use barrel export instead of deep path - Add setSessionName to mock CommandContext - Add getSessionTitle to SessionService mock in useResumeCommand tests - Update renameCommand tests for auto-generate title behavior - Update InputPrompt snapshots Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(session): add head+tail dual-read, string-level extraction, and finalize mechanism - Add sessionStorageUtils with extractLastJsonStringField() for fast string-level JSON field extraction without full parse - Add readHeadAndTailSync() to read first and last 64KB of session files - Replace readCustomTitleFromFile() with readSessionTitleFromFile() using head+tail dual-read (tail customTitle > head customTitle) - Add finalize() to ChatRecordingService as single entry point for re-appending session metadata on any session departure - Call finalize() on resume, session switch, and shutdown - Export sessionStorageUtils from core package Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(session): show filtered picker when /resume <title> matches multiple sessions Previously, multiple title matches opened the full session picker, forcing the user to re-find their session. Now the matched sessions are passed through as initialSessions to the picker, skipping the full listSessions() load and showing only the relevant results. Also clears sessionName on /clear so new sessions don't carry stale title tags from the previous session. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ui): use stringWidth for CJK-safe border alignment in input prompt topRightLabel.length counts UTF-16 code units, not terminal columns. CJK characters take 2 columns but .length returns 1, causing the border line to overflow. Use string-width for correct display width. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(session): address remaining PR #3093 review feedback - Add SESSION_TITLE_MAX_LENGTH shared constant in core, replace hardcoded 200 in CLI/ACP/VSCode/WebUI - Add title length validation to ACP renameSession endpoint - Make recordCustomTitle return boolean; renameCommand checks it before updating UI to prevent silent data loss - Add gitBranch to VSCode rename record for consistency with CLI - Remove misleading "enforce kebab-case" comment - Remove duplicate JSDoc on topRightLabel Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(ui): add animated dots to session name generation loading indicator The static "Generating session name…" text gave no visual feedback that the operation was in progress. Cycle through ".", "..", "..." every 500ms so users can tell the LLM call is still running. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * feat(cli): add /tag as alias for /rename command Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(vscode): add loading overlay when switching to historical conversations Adds isSwitchingSession state and sessionLoadComplete message to show a loading transition while session history is being rehydrated via ACP. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(vscode): add 15s timeout fallback for session switching loading state Prevents loading overlay from getting stuck indefinitely if sessionLoadComplete message is never received. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(core): fix extractLastJsonStringField offset tracking and add lineContains filter 1. Track global character offset across both pattern variants so the truly last match wins (previously the second pattern scan could overwrite a later match from the first pattern). 2. Add optional lineContains parameter to scope matches to lines containing a marker (e.g. "custom_title"), preventing false matches from user content that happens to include a "customTitle" field. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * chore(cli): add i18n import to DialogManager Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(vscode): align currentConversationId with webview on fallback restore When session/load falls back to creating a fresh ACP session, backend was tracking the new ACP id while the webview still viewed the archived sessionId. That desync caused delete/rename/title-update to target the wrong session during the fallback window, and prevented the post-first- message sync path from firing because the two ids were pre-aligned. Keep currentConversationId pointing at the archived sessionId until the existing stream-end sync flips both sides to the live ACP id on the first user message. Matches the pattern already used by the offline branch. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(core): exhaustive scan in findSessionsByTitle to avoid mtime-boundary misses listSessions() paginates with an mtime-only cursor and strict `<` filter. When several session files share the same mtime across a page boundary, the next page's filter drops them, so --resume <title> could silently miss valid matches. Scan all session files directly for title lookup, with filename as a stable tie-breaker. Also check the (cheap) custom title before the full hydration pass (first-record read, project filter, message count, prompt extraction) so non-matching sessions skip the extra I/O. listSessions() itself is left alone: its cursor crosses ACP/webview package boundaries as a number and this edge case only affects UI display order, not data loss. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(acp): plumb listSessions page size through _meta VSCode companion passes `size` to acpConnection.listSessions, but the ACP spec's ListSessionsRequest schema has no `size` field, so the SDK's zod validator strips it before the agent handler sees it. The agent then only forwarded `cursor` to SessionService.listSessions, silently ignoring the caller's page-size intent. Carry page size through `_meta.size` on both sides, matching the pattern already used for other Qwen Code ACP extensions (e.g. the filesystem service's `_meta.bom` / `_meta.encoding`). `_meta` is typed as an open record in the ACP schema, so extra keys survive validation. Co-Authored-By: Qwen-Coder <noreply@qwen.com> * fix(webui): avoid unintended rename when canceling with Escape The rename input auto-submits on blur, and pressing Escape also triggers blur (via setRenamingSessionId(null) unmounting the input). Because state updates are async, the blur handler's handleRenameSubmit could still read the pre-Escape renameValue from its closure and call onRenameSession, turning a cancel into an accidental rename. Track cancellation via an isCancelingRenameRef flag: set it in the Escape branch, and have onBlur short-circuit when the flag is true, then reset it. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(vscode-ide-companion): clear switch timeout on unmount The 15s session-switch fallback timer was only cleared on the next call to setIsSwitchingSession. If the webview is torn down mid-switch, the timer stays alive and later fires setIsSwitchingSessionRaw(false) on an unmounted hook. Add a useEffect cleanup to clear any pending timer on unmount. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(cli): break handleResume/slashCommandActions circular dep slashCommandActions (useMemo) depends on handleResume, but handleResume was declared after useSlashCommandProcessor so it could call setAwayRecapItem(null). useSlashCommandProcessor itself consumes slashCommandActions, closing a three-way cycle that tsc catches as TS2448 "used before declaration" once #3478's AppContainer changes land in main and get auto-merged into open PRs. Move handleResume above slashCommandActions and route the recap clear through a ref that a later useEffect syncs with setAwayRecapItem. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(core): scan full file when title is not in tail window Replace the head+tail dual-read with readLastJsonStringFieldSync: scan the tail first and return on hit, otherwise stream the whole file and return the last match. Closes the blind spot where a custom_title record landing between the head and tail windows would be missed on large session files. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <noreply@qwen.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: Qwen-Coder <noreply@alibabacloud.com> |
||
|
|
d1c8dff4d2
|
feat(arena): add comparison summary for agent results (#3394)
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
Adds a summary view that runs after Arena agents finish, so users can compare model outputs without opening each agent's conversation first. Summary surface: - Agent status overview - Files changed in common vs. unique to one agent - Per-agent approach summary generated through that agent's own provider - Token / runtime / line-change / file-count metrics Selection dialog now supports: - p — toggle preview for the highlighted agent - d — toggle detailed diff - Enter — select winner - x — discard all results - Esc — cancel Approach summary generation: - Each agent's summary is generated through that agent's own content generator, keeping mixed-provider Arena sessions within their respective auth boundaries - 20s timeout + AbortController per agent, bounded prompt inputs (finalText 2K, transcript 6K, diff 6K) - Falls back to a deterministic "Changed N files ..." summary when no per-agent generator is available or on error Diff summary now handles binary, rename-only, and mode-only diffs; the previous heuristic required textual +/- hunks and would have dropped those. Resolves #2559 |
||
|
|
e49867a762
|
feat(vscode): replace OAuth with Coding Plan / API Key provider setup (#3398)
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
* refactor(core): move codingPlan constants from cli to core package Extract Coding Plan region configs, model templates, and utility functions into packages/core/src/constants/ so both CLI and VSCode extension can import from a shared source of truth. * refactor(cli): import codingPlan constants from core instead of local path Update all CLI files to import CodingPlanRegion, CODING_PLAN_ENV_KEY, and related utilities from @qwen-code/qwen-code-core, replacing the local ../../constants/codingPlan.js imports. * feat(vscode-ide-companion): replace login flow with provider setup via VSCode Settings Replace the OAuth-based login command with a settings-driven provider configuration flow. Users now configure Coding Plan or API Key providers through VSCode Settings (qwen-code.*), which auto-syncs to ~/.qwen/settings.json. - Rename login command to auth, opening VSCode Settings panel - Add /auth2 interactive flow (QuickPick + InputBox) - Add ProviderSetupForm onboarding component with inline config - Add bidirectional sync between VSCode settings and ~/.qwen/settings.json - Add settingsWriter service for direct settings.json read/write - Add VSCode configuration schema (provider, apiKey, region, model, etc.) - Update all login/session messages to use auth terminology * refactor(vscode-ide-companion): rename auth2→auth, remove dead code, fix sync guard - Rename auth2 to auth for all message types, handlers, and slash command - Remove unused InfoBanner.tsx (128 lines, no references) - Remove dead openProviderSettings handler (no callers) - Remove redundant qwen-code.baseUrl VSCode setting (already in modelProviders) - Replace unreliable setTimeout(500) sync guard with await Promise.all + finally - Clean up old authHandler/setAuthHandler in favor of authInteractiveHandler * refactor(vscode-ide-companion): remove dead VSCode Settings plumbing, simplify sync - Remove qwen-code.modelProviders and qwen-code.model from package.json (model switching handled by chat UI's /model command, not VSCode Settings) - Remove connectWithSettings message handler and plumbing (no webview component sends this message type) - Remove handleConnectWithSettings method from WebViewProvider - Simplify syncVSCodeSettingsToQwenConfig: only sync provider/apiKey/region - Simplify syncQwenConfigToVSCodeSettings: only populate provider/apiKey/region - Simplify QwenSettingsForVSCode interface: remove modelProviders and model - Improve Onboarding UI: logo above card, better hierarchy, arrow icon on button * fix(vscode-ide-companion): add missing vscode.workspace mock in test Add onDidChangeConfiguration and getConfiguration to the vscode.workspace mock in WebViewProvider.test.ts to fix CI test failures. * fix(vscode-ide-companion): clean up stale coding plan state, add auth cancel handling, add tests - Clear CODING_PLAN_ENV_KEY and codingPlan metadata when switching to api-key mode - Add authCancelled notification when QuickPick/InputBox is dismissed - ProviderSetupForm resets button state on authCancelled - syncVSCodeSettingsToQwenConfig returns false for api-key mode (no-op) - Fix Onboarding vertical centering (flex-1 min-h-0) - Import from @qwen-code/qwen-code-core top-level instead of deep paths - Add tests: settingsWriter, ProviderSetupForm cancel, AuthMessageHandler cancel, WebViewProvider sync - Fix redundant ternary in pick() helper * fix(vscode-ide-companion): force center Onboarding against parent override Parent container uses [&>*]:items-start and [&>*]:text-left which overrides Tailwind classes. Use inline style for alignItems/justifyContent/textAlign to ensure Onboarding is always centered both horizontally and vertically. * fix(vscode-ide-companion): bundle onboarding logo * test(vscode-ide-companion): add png loader to bundle test * fix(vscode-ide-companion/webview): avoid redundant auth sync reconnects * fix(vscode-ide-companion/webview): fix auth sync typecheck * docs(vscode-ide-companion): clarify auth restoration flow * fix(webui): use bracket access for permission drawer plan content * fix(vscode-ide-companion): guard authSuccess emission on actual auth state After reconnecting in handleAuthInteractive, doInitializeAgentConnection may return without throwing even when credentials are rejected (it sends authState:false internally and returns early). Previously we unconditionally emitted authSuccess, which contradicted the failed auth state and could briefly show a success toast before re-opening the auth flow. Now we check this.authState after reconnection: only emit authSuccess when authentication actually succeeded, otherwise emit authError with a clear credentials message. Addresses review feedback from PR #3398. * fix(vscode): address auth setup review feedback * fix(vscode-ide-companion): guard concurrent auth flows, merge model providers - Add authFlowActive mutex and autoAuthTimer to WebViewProvider so startInteractiveAuth() cancels the deferred auto-auth timeout, preventing two overlapping QuickPick flows from a single command. - Change writeModelProvidersConfig() to merge new entries with existing non-target models (different envKey) instead of replacing the entire array, preserving unrelated providers like Coding Plan. * fix(vscode-ide-companion): handle apiKey clearing as de-auth signal, fix auto-auth race, clean imports - Add clearPersistedAuth() to settingsWriter.ts: removes selectedType, API keys, and coding plan metadata from ~/.qwen/settings.json - Config change handler now detects empty apiKey with active agent and triggers de-auth: clear credentials, disconnect, update authState - Auto-auth timer callback now properly sets authFlowActive mutex to prevent concurrent auth flows with startInteractiveAuth() - Add test covering the de-auth path (clearPersistedAuth + disconnect) - Fix import formatting in 7 CLI files (spacing, trailing commas) - Remove duplicate comment in attemptAuthStateRestoration() * fix(vscode-ide-companion): scope de-auth to apiKey changes only The previous de-auth logic triggered on any auth-related setting change where syncVSCodeSettingsToQwenConfig() returned false. For api-key providers this is the normal path (interactive auth owns config), so changing codingPlanRegion or provider would incorrectly wipe OPENAI_API_KEY. Now the de-auth branch only fires when e.affectsConfiguration('qwen-code.apiKey') is true AND the value is empty, preventing false-positive credential clearing. Add regression test: non-apiKey setting changes on an api-key provider must not trigger clearPersistedAuth or disconnect. * fix(vscode-ide-companion): add disconnect to mock type to fix CI typecheck The hoisted mockQwenAgentManagerInstances type was missing the disconnect property, causing TS2339 in the de-auth test assertions. |
||
|
|
5a43efcae4
|
fix(editor): detect Zed.app on macOS when CLI is not in PATH (#3303)
* fix(editor): detect Zed.app on macOS when CLI is not in PATH On macOS, Zed editor is typically installed via Homebrew or direct download, but the CLI command 'zed' is not automatically added to PATH. This fix adds detection for Zed.app bundle at: - /Applications/Zed.app - ~/Applications/Zed.app When the CLI is not found but the app bundle exists, the code now falls back to using the CLI inside the app bundle at Contents/MacOS/zed. Fixes #3287 * fix(editor): use shared getEditorExecutable in useLaunchEditor - Export getEditorExecutable() from editor.ts for use by both getDiffCommand and useLaunchEditor - Updated useLaunchEditor.ts to use getEditorExecutable instead of its own implementation - Updated tests to be platform-agnostic for macOS app bundle path testing - Fixes: Zed on macOS is now detected when installed via app bundle (not just CLI in PATH) * fix(editor): use correct Zed CLI path (Contents/MacOS/cli) - Changed from Contents/MacOS/zed (GUI binary) to Contents/MacOS/cli (actual CLI) - The GUI binary does not support --wait/--diff flags - Updated tests to verify correct CLI path with regex matching for cross-platform * style(editor): fix prettier trailing whitespace issues Trailing spaces and array line-wrapping in zed macOS detection code. * fix(editor): return null when editor not found + remove unused var - getEditorExecutable now returns null (not fallback string) so useLaunchEditor error handling actually works - remove unused getAppBundleCliPath in test file (typecheck fix) * fix: add vitest globals to eslint config for test files * fix: remove duplicate empty test with orphan toEqual call * fix: resolve ESLint errors in editor.test.ts (arrow-body-style, no-useless-escape) * chore: remove debug script check_braces.js * fix: sync checkHasEditorType with getEditorExecutable, remove pr-body.md - Replace zedAppExists() check in checkHasEditorType with getEditorExecutable() !== null, keeping availability detection and execution in sync (fixes partial install false positive) - Remove unused zedAppExists() function - Remove scratch file pr-body.md * fix(editor): defer os.homedir() call to avoid breaking tests with incomplete node:os mocks The zedMacOsPaths constant was calling homedir() at module initialization time, which caused 'homedir is not a function' errors in CLI test files (systemInfo.test.ts, shellCommandProcessor.test.ts) that mock node:os without providing a homedir mock. Fix: convert zedMacOsPaths from a constant to a lazy function getZedAppPaths() that computes the paths only when called. --------- Co-authored-by: lamb <906276457@qq.com> |
||
|
|
519e5aa1de
|
fix(core): recover from truncated tool calls via multi-turn continuation (#3313)
* fix(core): recover from truncated tool calls via multi-turn continuation (#3049) When large tool calls (e.g., WriteFile with big HTML) exceed the output token limit, the model's response gets truncated and required parameters like file_path are missing. Previously this surfaced as a confusing "params must have required property" error. Three-layer defense: 1. Escalate to model's actual output limit (not fixed 64K). Models with 128K output (Claude Opus, GPT-5) now use their full capacity. 2. Multi-turn recovery: if the escalated response is still truncated, keep the partial response in history and inject a recovery message ("Resume directly — pick up mid-thought") so the model continues from where it left off. Up to 3 recovery attempts before falling back to the tool scheduler's guidance. 3. Stronger truncation guidance as fallback: "you MUST split" instead of "consider splitting". Also fixes: - Clear toolCallRequests on RETRY to prevent duplicate tool execution - Add isContinuation flag to RETRY events so the UI preserves text buffers during recovery (continuation) but resets them during escalation (fresh restart) - Catch errors during recovery to prevent dangling history entries * docs: update adaptive output token escalation design for recovery mechanism Update the design doc to reflect: - Escalation now targets model's actual output limit (64K floor) - Multi-turn recovery loop after escalation (up to 3 attempts) - isContinuation flag on RETRY events - Recovery error handling (pop dangling message, break) - Updated constants table and model-specific escalation limits - New design decision: why multi-turn recovery over progressive escalation * fix: remove competitor reference from code comment * fix: address review feedback on recovery mechanism Three correctness fixes from @tanzhenxin's review: 1. Partial text lost during continuation (useGeminiStream.ts): On continuation RETRY, setPendingHistoryItem(null) cleared the pending gemini item. The next Content event then saw a null pending item, created a fresh one, and reset geminiMessageBuffer = eventValue — discarding the preserved partial text. Now the pending item AND buffers are kept on continuation, so the continuation appends. 2. Recovery on truncated tool-call turns (geminiChat.ts): When the truncated turn already contains a complete functionCall, appending a user recovery message produces model(functionCall) → user(text) with no intervening functionResponse — an invalid API sequence. Now recovery skips turns with functionCall parts and defers to the tool scheduler's layer-3 fallback. 3. Recovery errors swallowed after partial chunks (geminiChat.ts): If a recovery attempt yielded chunks then failed, the catch block broke without emitting any terminal signal, leaving the UI with partial text and no Finished event. Now emits a synthetic finishReason=STOP chunk in the catch so the UI gets a proper terminal signal. * test: add coverage for output token recovery loop Four targeted tests for the recovery mechanism introduced in the truncated-tool-call-recovery PR: 1. Recovery loop fires when escalated response is also truncated: initial MAX_TOKENS → escalation MAX_TOKENS → recovery STOP. Verifies two RETRY events (one escalation, one continuation) and three API calls. 2. Recovery is skipped when truncated turn contains a functionCall: prevents the invalid model(functionCall) → user(text) sequence. Verifies no continuation RETRY and history ends with the functionCall intact. 3. Recovery attempts are capped at MAX_OUTPUT_RECOVERY_ATTEMPTS (3): persistent MAX_TOKENS triggers exactly 5 API calls (1 initial + 1 escalation + 3 recovery). 4. Recovery catch block emits synthetic STOP chunk and pops dangling user message: when a recovery attempt fails (empty stream → InvalidStreamError), the UI gets a terminal signal and history ends on the model turn, not a dangling user recovery message. * test: cover cross-iteration functionCall detection in recovery loop Existing tests cover the functionCall guard when both initial and escalated responses have functionCall. This adds a test for the cross-iteration case: iter 1 returns text (recovery proceeds), iter 2 returns functionCall (recovery must break before iter 3). Verifies: - API called exactly 4 times (1 initial + 1 escalation + 2 recovery) - History ends with the functionCall model turn, not a dangling user recovery message - Iter 3's user recovery message is never pushed (guard fires at top of loop before recoveryCount increment) * fix(core): cast synthetic STOP chunk via unknown for TS2352 The object literal {candidates, content, parts} doesn't structurally overlap enough with GenerateContentResponse for TypeScript's strict narrow cast. Casting through 'unknown' is required per TS2352. Build error from CI: src/core/geminiChat.ts(651,24): error TS2352: Conversion of type '...' to type 'GenerateContentResponse' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. * test(core): tighten recovery history integrity assertions Strengthen the "pop dangling recovery message" test to catch any future regression that leaves consecutive same-role entries or an empty last-model placeholder in history — conditions providers reject on the next turn. * fix(core): coalesce recovery pairs to avoid leaking control prompt Previously every output-token recovery iteration left a (user, model) pair in durable history where the user turn was the internal OUTPUT_RECOVERY_MESSAGE control prompt. That prompt was then visible to every later turn, biasing responses and polluting compression, replay, and export. Track successful recovery iterations and, after the recovery loop, fold each completed pair back into the preceding model turn via a new `coalesceRecoveryPairs` helper. Failed iterations already pop their user turn in the catch block, so they need no coalescing. Adds a targeted test that runs escalation + two successful recovery iterations + a clean STOP, and asserts the merged history has exactly one user turn and one model turn, no trace of the control prompt text, and content ordered as B (escalation) + C + D. |
||
|
|
c25136f0ef
|
feat(cli): display real-time token consumption during streaming (#2742) (#3329)
* feat(cli): display real-time token consumption during streaming (#2742)
Show ↓/↑ token count in the spinner during model execution:
- ↓ when receiving content, ↑ when waiting for API response
- Accumulates across the whole turn (tool calls don't reset)
- Includes agent/subagent token consumption
- Uses useAnimationFrame hook (50ms polling) to avoid flickering
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix: address review feedback for real-time token display
- Replace unsafe type assertion with proper type guard in Composer
- Fix license header in useAnimationFrame.ts to match project standard
- Clarify tokenCount is replaced (not accumulated) per USAGE_METADATA event
- Use multi-line JSDoc format for isReceivingContent prop
- Improve re-sync comment in useAnimationFrame hook
- Revert unrelated streamingState dep change in AppContainer
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): use output-only tokens and accumulate across subagent rounds
Subagent token display had two bugs:
- Used totalTokenCount (input+output) instead of candidatesTokenCount
(output-only), causing mixed units when aggregated with main stream
- Overwrote tokenCount per round instead of accumulating, so multi-round
subagents only showed the last round's count
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(cli): smooth token counter animation and include tool args
Interpolate displayed token count toward the real value (3/frame for
small gaps, ~20% for medium, 50 for large) so chunked arrivals like
tool-call args no longer cause visible jumps. Also accumulate tool
call args JSON length into the streaming estimate, matching Claude
Code's input_json_delta handling.
Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>
* fix(cli): scope token animation re-renders to LoadingIndicator
The 50ms useAnimationFrame poll lived in Composer, causing its entire
subtree (InputPrompt, Footer, KeyboardShortcuts) to reconcile 20×/sec
during streaming. Combined with the spinner and streamed text deltas,
ink redrew enough lines to produce visible terminal flicker.
Move the animation hook into LoadingIndicator so only that component
re-renders per frame, and slow polling to 100ms to match the spinner
cadence.
Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>
* fix: address review nits on token display
1. AgentResultDisplay.tokenCount jsdoc said "(input + output)" but the
value has been output-only since
|
||
|
|
07bd5c41cb
|
fix(mcp): make the OAuth authorization URL clickable when wrapped (#3489)
* fix(mcp): render OAuth URL as OSC 8 hyperlink so it stays clickable when wrapped Closes #3470. The MCP OAuth flow previously pushed the authorization URL through the generic display-message list, where Ink rendered it as plain text. When the URL exceeded the terminal width it got hard-wrapped into the message buffer, and most terminals could no longer detect it as a single hyperlink (cmd/ctrl+click did nothing, selecting it pulled in extra whitespace). Render the URL as an OSC 8 hyperlink in AuthenticateStep instead, and stop duplicating it through the display-message stream when an event emitter is available. Terminals that support OSC 8 (iTerm2, WezTerm, Kitty, Windows Terminal, VS Code, GNOME Terminal, …) now treat the URL as a single clickable link even when it visually wraps; terminals without OSC 8 support ignore the escapes and fall back to the existing "press c to copy" affordance. * fix(mcp): pre-split OAuth URL so every wrapped line stays clickable Wrapping the whole URL in a single OSC 8 hyperlink and letting Ink / wrap-ansi break the line produced two bugs observed in iTerm2 etc.: only the first visible segment was a hyperlink (wrap-ansi re-emits SGR codes across wraps but does not re-open OSC 8 links), and the remaining URL characters overflowed past the dialog border because wrap-ansi was unable to break the unbroken URL token within the container width. Manually slice the URL into chunks of `columns - 8` characters (MCPManagementDialog's container width) and render each chunk as its own OSC 8 hyperlink with `wrap="truncate"`. Every visible line now carries a complete hyperlink pointing at the same URL, and no line exceeds the container width. * fix(mcp): terminate OSC 8 hyperlinks with BEL so Ink preserves them Ink's renderer tokenizes text through @alcalzone/ansi-tokenize, which only recognizes OSC 8 hyperlink escapes terminated with BEL (\x07). The ST terminator (ESC \\) we were using is valid per the OSC 8 spec but the tokenizer falls through and treats the escape bytes as regular characters. That explains the two symptoms seen after the previous fix: - Only the first URL segment rendered as a clickable hyperlink. The rest of the lines had their opening \\x1b]8;; bytes tokenized as chars, so their hyperlink wrap was lost. - The dialog's right border disappeared because the mangled escape bytes consumed grid cells, pushing the container width past `columns - 8` and shoving the border off-screen. Switch the helper to the BEL-terminated form. Ink now sees each line's OSC 8 wrap as a proper zero-width code, every wrapped line stays clickable, and the border is no longer displaced. * fix(mcp): render OAuth URL via <Static> as a single unwrapped line The per-line OSC 8 approach didn't make lines past the first clickable in real terminals. Root cause: Ink's renderer runs text through @alcalzone/ansi-tokenize, which: - Only accepts OSC 8 sequences with empty params (`\x1b]8;;URL\x07`). Any `id=` form is parsed as a bogus SGR code and the remainder leaks out as visible characters. Without an `id=` grouping parameter, terminals like iTerm2 don't reliably stitch adjacent OSC 8 escapes together as one hyperlink. - Re-emits styles per Ink row via styledCharsToString, so even when each slice carried a self-contained OSC 8 wrap, terminals still treated each visual line as an independent hyperlink that only the first row reliably activated. Emit the URL through Ink's `<Static>` component instead, inside a `<Box width={url.length}>`. Ink sees a single logical line that doesn't need wrapping, so it hands the terminal one OSC 8 open, the whole URL, and one close. The terminal then soft-wraps that line visually, and the OSC 8 hyperlink state is carried across every wrap — every visible line is clickable. `<Static>` writes once above the dynamic dialog (scrollback-safe) and isn't touched by re-renders, which also avoids the flicker we'd get from repeatedly re-emitting the escape sequence inside the live tree. * fix(mcp): render OAuth URL as live row so it clears on dialog dismissal The previous <Static> emission made the URL stay permanently in the scrollback after the OAuth flow finished — e.g. after the dialog was dismissed the URL was still sitting above the prompt. Switch to a normal (live) Ink row: a Box sized to the URL length holding a single OSC 8 wrapped Text. Ink doesn't wrap the row (maxWidth == content width), so it hands log-update one long line; log-update's wrap-ansi pass then wraps it at terminal width and re-emits the OSC 8 escape at every wrap boundary, so every visible wrapped line is clickable. Because this is a regular child of the dialog, log-update tracks its height and erases it cleanly when the AuthenticateStep unmounts (auth succeeds / user backs out / dialog closes). * fix(mcp): pre-split OAuth URL so the live row clears cleanly The wide-Box live approach left dialog fragments in the scrollback: Ink ships its own log-update.js (packages/cli/.../ink/build/log-update.js) which counts erase height with output.split('\n').length and does NOT run wrap-ansi. A single Ink row that exceeds terminal width wraps visually but the erase still covers only one terminal line, so authState transitions (auth success, Esc-to-back, dialog dismiss) leave the top rows of the previous frame behind. Go back to pre-slicing the URL into chunks sized to the dialog content width (columns - 8) and rendering each chunk as its own Ink row with its own OSC 8 wrap. Log-update's row count then matches the visible row count, so erase is clean on every transition. Terminals that group adjacent OSC 8 sequences will still treat the whole URL as clickable; those that don't at least keep the first slice clickable, and the existing "press c to copy" affordance covers the rest. * fix(mcp): commit to Static-rendered URL outside the dialog Stop flip-flopping between in-box and out-of-box URL rendering. Every in-box attempt hit one of two walls: - Per-slice OSC 8 rows: each Ink row is its own self-contained hyperlink, but some terminals (seen with the reporter's) only register the first adjacent OSC 8 without an id= parameter as clickable. Ink's @alcalzone/ansi-tokenize rejects OSC 8 with params, so id= grouping is not deliverable. - Wide-Box overflow rows: the single OSC 8 wrap keeps every wrapped line clickable because the hyperlink state persists across the terminal's soft-wraps, but Ink ships its own log-update.js that counts erase height by output.split('\n').length and never runs wrap-ansi. When the row visually wraps but Ink counts it as 1 row, transitions (auth success / Esc / dismiss) erase too few lines and leave dialog fragments in the scrollback. Render the URL through <Static> above the dialog: it writes once, outside log-update's tracking, so the terminal soft-wraps a single OSC 8 hyperlink and every visible line stays clickable. The trade-off is that the URL stays in the scrollback after the dialog dismisses (Static is append-only); that is acceptable given the URL is no longer sensitive once auth has completed, and it avoids the click-failure and residue problems of the other approaches. * fix(mcp): print OAuth URL via useStdout, erase on unmount Drop <Static> (which persisted the URL in the scrollback forever) and print the authorization URL directly with Ink's `write` (useStdout) instead. Ink's writeToStdout clears the live frame, writes our bytes into the scrollback, and re-renders the frame below, so the URL goes out in a single OSC 8 hyperlink sequence and the terminal's soft-wrap preserves the hyperlink state across every wrapped row — every visible line stays clickable. On unmount (auth success, Esc, dialog dismiss) we use the same `write` path to push a cursor-up + eraseLines sequence that removes the URL rows (plus the leading/trailing blank separators) before log-update redraws the now-smaller live frame. Net effect: URL shows above the dialog while authenticating, disappears cleanly when the dialog goes away, and every wrapped line is clickable throughout. * fix(mcp): period-terminate prompt and restore wrap warning Now that the OAuth URL renders above the dialog (outside the message list), the in-dialog prompt no longer leads into the URL on the next line — rename the i18n key from "…into your browser:" to "…into your browser." and re-add the "Make sure to copy the COMPLETE URL — it may wrap across multiple lines." warning that was dropped when the URL was first moved out of displayMessage. Translations in de/en/fr/ja/pt/ ru/zh are updated to match and to point at the URL "above" rather than "following". * fix(mcp): correct OAuth URL erase count on unmount The previous logic wrote the URL as `\n${URL}\n` (leading + trailing newlines) and erased `urlVisualLines + 2` rows on unmount, but the leading blank and the trailing "\n" don't both occupy their own rows — the trailing newline just moves the cursor to where the dynamic UI is re-rendered. For a typical URL whose length isn't an exact multiple of the terminal width this left the erase off-by-one and wiped a row above the dialog (e.g. a piece of the command prompt). Drop the leading `\n` (no real visual benefit) and compute the erase count as `urlVisualLines + (autoWrapOverflow ? 1 : 0)`. The overflow term handles the aligned edge case where the terminal auto-wraps past the last URL char, leaving a blank row between URL and re-rendered dynamic UI that also needs erasing. Also drop the stale comment about Ink's ansi-tokenize restricting OSC 8 terminator choice — we now bypass Ink's tokenizer via useStdout, so BEL is just the more compatible terminator. * fix(mcp): pass OAuth URL hyperlink through multiplexer wrapper Inside tmux or GNU screen the raw OSC 8 hyperlink escape is intercepted by the multiplexer and never reaches the host terminal — users see the URL as plain text, exactly the bug this PR is trying to fix. The existing `wrapForMultiplexer` helper (already used for OSC 52 clipboard writes) wraps the sequence in a DCS passthrough envelope that tmux / screen forward to the host. Apply the same helper to `osc8Hyperlink` so tmux / screen users get clickable links for every wrapped line as well. Outside a multiplexer the helper is a no-op, so native terminals are unchanged. Also note in a comment that the captured `stdout.columns` goes stale if the terminal is resized during the OAuth flow; this is acceptable for a sub-minute flow on ASCII-only authorization URLs. * docs(mcp): note tmux 3.3+ allow-passthrough requirement * fix(mcp): render OAuth URL inside dialog box Replace the useStdout().write + cursor-up/eraseLines scrollback approach with an in-dialog <Box><Text>{osc8Hyperlink(url)}</Text></Box>. Removes the Ink dynamic-frame interleave and the column-width erase bookkeeping; the URL is owned by the dialog, so it disappears with it. * refactor(mcp): drop redundant Fragment around single Text * revert(mcp): restore original OAuth prompt wording URL now renders inside the dialog box, so the "copy and paste this URL into your browser:" prompt no longer needs the period-terminated / "URL above" rewording. Revert the i18n keys and localized strings; keep the event-driven dispatch so the URL isn't also pushed through displayMessage (which would double-render in the UI). * fix(mcp): sanitize URL/label before embedding in OSC 8 sequence An unescaped \x07 (BEL) or \x1b (ESC) in the URL or label would terminate the OSC 8 envelope early and let the tail bytes through as interpretable terminal escapes. authUrl is normally built via URL.toString() which percent-encodes controls, but the authorization endpoint itself comes from server-controlled OAuth discovery, so treat the input as untrusted and strip C0 + DEL before splicing. |
||
|
|
afbb5e71db
|
fix(cli): rework session recap rendering and add blur threshold setting (#3482)
* feat(cli): make recap away-threshold configurable The 5-minute blur threshold was hard-coded. Confirmed from Claude Code's own binary (v2.1.113) that 5 minutes is their default as well (and that they shift to 60 minutes when 1h prompt-cache is active) — so the default stays, but expose it as `general.sessionRecapAway ThresholdMinutes` for users who briefly alt-tab often and don't want recaps piling up, or who want to lower it for testing. Non-positive / unset values fall back to the 5-minute default, so dropping the key has the same behavior as before. * fix(core): align recap prompt with Claude Code (1-2 sentences, ≤40 words) The earlier "exactly one sentence, 80-char cap" was an over-correction to a single in-the-moment ask. Going back to it: the natural shape of "current task + next action" is two clauses, and forcing them into a single sentence either crams them with a semicolon or drops the next action entirely on complex sessions. Adopt Claude Code's prompt verbatim (extracted from the v2.1.113 binary): "under 40 words, 1-2 plain sentences, no markdown. Lead with the overall goal and current task, then the one next action. Skip root-cause narrative, fix internals, secondary to-dos, and em-dash tangents." Add a Chinese-budget note (~80 chars) and keep the <recap>...</recap> wrapping that protects against reasoning-model preambles leaking into the UI. The sticky banner already re-measures controls height when the recap toggles, so a 2-line render lays out cleanly. Sweep "one-line" out of user-facing copy (settings description, slash-command description, feature docs, design doc) so the documentation matches the new shape. * fix(cli): restore "one-line" in user-facing recap copy Verified from the Claude Code v2.1.113 binary that the slash-command description IS literally "Generate a one-line session recap now" even though the underlying prompt allows 1-2 sentences. Claude Code is deliberately setting a tighter user expectation than the prompt guarantees, which keeps the surface feel "glanceable". Mirror that asymmetry: keep the prompt at 1-2 sentences (the previous commit) for behavioral parity, but put "one-line" back in the user- visible copy (slash-command description, settings description, user docs). Internal design doc keeps the accurate "1-2 sentence" wording. * fix(cli): render recap inline in history to match Claude Code Earlier I read the user's complaint that the recap "scrolled away" as "the recap should be sticky above the input box," and built a sticky banner accordingly. Disassembly of the Claude Code v2.1.113 binary shows the actual behavior is the opposite: their away_summary is a plain `type:"system", subtype:"away_summary"` message dispatched through the standard message renderer (no Static, no anchor, no flexbox pinning) — it scrolls with the conversation like every other system message. Tear out the sticky-banner machinery so recap matches that: - Recap is back in the `HistoryItemWithoutId` union and `addItem`'d into history (both from `/recap` and from auto-trigger), so it serializes into session saves and behaves like every other history item — no special clear paths, no resume-wrapper, no layout-effect re-measure dance. - `useAwaySummary` takes `addItem` again instead of a setter callback. - `AwayRecapMessage` renders the way Claude Code does: a 2-column gutter with `※`, then bold "recap: " and italic content, all in dim color. Drop the prior `StatusMessage`-shaped layout that fused prefix and label into "※ recap:". - Remove the AppContainer plumbing, the slashCommandProcessor state, the UIStateContext fields, the DefaultAppLayout / ScreenReader placement blocks, the test-utils mocks, and the noninteractive stub. Restore `useResumeCommand.handleResume` to a void return since callers no longer need the success boolean. Sweep the design doc so the architecture diagram, files table, and hook deps reflect the inline-history flow. * fix(cli): dedupe back-to-back auto-recaps with no new user turns between Two consecutive blur cycles, each over the threshold but with no new user activity in between, would each fire their own auto-recap and add two near-duplicate entries to history (same task, slightly different wording from temperature-driven LLM variance). Reported case: leaving the terminal twice while a /review of one PR was still on screen produced two recaps both about that same review. Add a `shouldFireRecap` gate before kicking off the LLM call: - Need at least 3 user messages in history total (don't fire on a near-empty session). - If a previous away_recap is already in history, need at least 2 new user messages since that one before another can fire. Same shape as Claude Code's `Ic1` gate (`Sc1=3`, `Rc1=2`). Read history through a ref so this isn't in the effect's deps and the effect doesn't re-run on every message. * fix(cli): type useResumeCommand.handleResume as Promise<void> Per gemini review on #3482: the interface declared this as `() => void` but the implementation is `async` and returns `Promise<void>`. The mismatch silently lost the chainable promise — tests had to launder it through `as unknown as Promise<void> | undefined` just to await. Tighten the interface to `Promise<void>` and drop the cast in the "closes the dialog immediately" test. * fix(cli): persist auto-fired recap to chat recording so /resume keeps it Per yiliang114 review on #3482: the manual `/recap` path persists across `/resume` because the slash-command processor records every output history item via `chatRecorder.recordSlashCommand({ phase: 'result', outputHistoryItems })`, but the auto path called `addItem` directly and bypassed that recorder. The result was an asymmetry where users who triggered recap manually saw it after `/resume`, while users whose recap fired automatically lost it. Mirror the manual recording from useAwaySummary's `.then` callback — record only the `result` phase (not invocation, since we don't want a fake `> /recap` user line replayed) with the away-recap item as the single output. Wrapped in try/catch because recap is best-effort and must never surface a failure to the user. Add useAwaySummary.test.ts covering: - the recording path is taken on a successful auto-trigger - the dedup gate (`shouldFireRecap`) suppresses the LLM call entirely, including the recording, when no new user turns happened since the last recap * fix(cli): cast recap item via spread to satisfy strict tsc --build CI's `tsc --build` (stricter than local `tsc --noEmit`) rejected the direct `item as Record<string, unknown>` cast: HistoryItemAwayRecap's literal `type: 'away_recap'` field doesn't overlap with `unknown`, TS2352. Use the `{ ...item } as Record<string, unknown>` spread pattern that the rest of the codebase (arenaCommand, slashCommandProcessor's serializer) already uses for the same SlashCommandRecordPayload field. |
||
|
|
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. |
||
|
|
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`. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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).
|
||
|
|
9de33dded3
|
feat(cli): add /doctor diagnostic command (#3404)
Closes #3018 |
||
|
|
c175fd3d4a
|
feat(core): enhanced loop detection with stagnation + validation-retry checks (#3236) | ||
|
|
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> |
||
|
|
eae247b50e
|
fix: display (#2766) | ||
|
|
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
|
||
|
|
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> |
||
|
|
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 |