Commit graph

5597 commits

Author SHA1 Message Date
tanzhenxin
a6b0b7e579
Revert "fix(cli): respect OPENAI_MODEL precedence in CLI model resolution (#3567)" (#3633)
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
This reverts commit 007a109db8.

The change made `OPENAI_MODEL` outrank `settings.model.name` when looking
up the active entry in `settings.modelProviders`. Combined with the core
resolver's `modelProvider > cli > env > settings` priority, this caused
a regression: a `/model` selection (which writes `settings.model.name`)
was silently overridden whenever `OPENAI_MODEL` was set in the user's
shell, with no warning surfaced.

Restoring the previous behavior — looking up the provider entry by
`argv.model || settings.model?.name` — preserves the implicit contract
that an explicit `modelProviders` config takes precedence over stale
shell defaults. Users without a `modelProviders` config are unaffected:
env vars still drive model selection through the core resolver.

See discussion on #3567.
2026-04-26 14:29:18 +08:00
Shaojin Wen
29887ddfef
fix(core): match DeepSeek provider by model name for sglang/vllm (#3613) (#3620)
Some OpenAI-compatible servers (notably sglang's deepseek-v4 jinja
template) crash on the array form of message content even when it
carries a single text block, with `TypeError: sequence item 0:
expected str instance, list found` at `encoding_dsv4.py:336`.

The DeepSeekOpenAICompatibleProvider already flattens content arrays
into joined strings in buildRequest, but isDeepSeekProvider only
matched on the official api.deepseek.com baseUrl. DeepSeek models
served behind sglang / vllm / ollama / etc. bypass the workaround
and hit the bug.

Extend the matcher to also detect by model name (case-insensitive
substring 'deepseek'), so any OpenAI-compatible endpoint serving a
DeepSeek model picks up the same content-format flattening.

Fixes #3613

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-26 13:17:34 +08:00
Shaojin Wen
569cfe10fa
fix(telemetry): use safeJsonStringify in FileExporter to avoid circular reference crash (#3630)
When --telemetry-outfile is configured, FileSpanExporter.serialize called
JSON.stringify directly on OTel ReadableSpan instances. The spans hold a
back-reference to BatchSpanProcessor (._shutdownOnce -> BindOnceFuture._that
-> BatchSpanProcessor), which forms a cycle and triggers
"TypeError: Converting circular structure to JSON" on every export. Combined
with DiagConsoleLogger, the error was repeatedly printed to stderr and
polluted the Ink TUI.

Switch FileExporter.serialize to the existing safeJsonStringify utility,
matching the upstream gemini-cli fix so future merges stay clean. Add a
focused regression test that mimics the BatchSpanProcessor cycle shape;
broader cycle behavior is already covered by safeJsonStringify.test.ts.

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-26 12:55:39 +08:00
Yan Shen
eea4e10eea
feat(cli): add sticky todo panel to app layouts (#3507)
* feat(cli): add sticky todo panel to app layouts

* fix(cli): hide sticky todos during feedback dialog
2026-04-26 12:21:30 +08:00
jinye
4be0234d10
docs(telemetry): clarify Alibaba Cloud console entry (#3498)
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
* docs(telemetry): clarify Alibaba Cloud console entry

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

* docs(telemetry): fix unreachable intl console URL and split new/legacy console guidance

- Replace unreachable tracing-sgnew.console.alibabacloud.com with the
  verified arms.console.alibabacloud.com for international users
- Separate OTLP endpoint retrieval steps by console version: new console
  uses Integration Center, legacy console uses Cluster Configurations →
  Access point information

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* docs(telemetry): align target example with current implementation

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

* docs(telemetry): clarify Alibaba Cloud OTLP setup

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

* docs(telemetry): remove stale TOC entry

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

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-26 07:40:35 +08:00
jinye
f7cfe53c6a
fix(core): preserve settings-sourced apiKey when registry model envKey is absent (#3495)
* fix(core): preserve settings-sourced apiKey when registry model envKey is absent (#3417)

On restart, `applyResolvedModelDefaults` unconditionally cleared the
apiKey resolved from `settings.security.auth.apiKey` (layer 4 fallback)
and only read from `process.env[model.envKey]`. When the provider-specific
env var was absent (e.g. key stored only in settings), the correctly
resolved key was discarded, causing a 401 error.

Now capture the previously-resolved apiKey before clearing and fall back
to it when `process.env[model.envKey]` is empty, but only for safe source
kinds (`settings` and general `env` without `via.modelProviders`).

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

* fix(core): also preserve CLI-sourced apiKey during syncAfterAuthRefresh

Address review feedback: keys passed via CLI flags (e.g. --openaiApiKey)
were dropped on restart because source kind 'cli' was not in the
fallback allowlist. Add 'cli' to the condition and a regression test.

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

* fix(core): move apiKey preservation from applyResolvedModelDefaults to syncAfterAuthRefresh

The previous fallback logic inside applyResolvedModelDefaults could leak
a settings/cli-sourced apiKey to a different provider when switching
models within the same authType (e.g. dashscope → openai). This is a
credential safety issue because the two providers may have different
baseUrls.

Move the save/restore logic to syncAfterAuthRefresh Step 1, guarded by
an `isUnchanged` check (same authType AND same modelId). This ensures:
- Restart scenario: apiKey preserved (same model, no change)
- Cross-provider switch: apiKey cleared (different modelId)

Also adds two cross-provider switch tests (settings-sourced and
CLI-sourced) per review feedback.

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

* fix(core): replace non-null assertion with truthiness guard and add cold-start test

- Replace `savedApiKeySource!` with a truthiness guard for safer
  source restoration
- Add test for cold-start scenario (previousAuthType undefined) to
  verify no key preservation occurs on first syncAfterAuthRefresh
- Fix stale "short-circuit" comment in programmatic key test

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

* fix(core): detect provider config hot-reload in isUnchanged check

When a model provider config is hot-reloaded (e.g. via Coding Plan
update) changing envKey or baseUrl while keeping the same model id,
the save/restore logic must not preserve the old apiKey. Extend the
isUnchanged guard to compare apiKeyEnvKey and baseUrl against the
resolved model, but only after applyResolvedModelDefaults has run at
least once (apiKeyEnvKey !== undefined). On first startup call these
fields are still unset, so the check is skipped to preserve the
settings/cli-sourced key correctly.

Adds two hot-reload tests (envKey change and baseUrl change).

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

* fix(core): use baseUrl source as hasBeenApplied signal for provider change detection

Replace `apiKeyEnvKey !== undefined` guard with `baseUrl source ===
'modelProviders'` to reliably detect whether applyResolvedModelDefaults
has been called before. This fixes two edge cases:

1. No-envKey models: hot-reload changing baseUrl was undetected because
   apiKeyEnvKey remained undefined. Now baseUrl source is checked.
2. Startup with envKey but omitted baseUrl: undefined !== default URL
   could falsely trigger isProviderChanged. Now skipped at startup
   since baseUrl source is not yet 'modelProviders'.

Updates hot-reload test fixtures to simulate post-apply state (baseUrl
source as 'modelProviders') and adds no-envKey hot-reload test.

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

* fix(core): shallow-clone savedApiKeySource to avoid mutation risk

Copy the ConfigSource object before applyResolvedModelDefaults runs,
so a future refactor that mutates source objects in place won't break
the save/restore logic.

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

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-26 07:37:56 +08:00
jinye
72c31d378d
fix(test): update rewind E2E Test 1 assertion after isRealUserTurn fix (#3622)
Test 1 asserted `say exactly GAMMA3` after pressing Up once in the
rewind selector, but that only passed because `/rewind` was incorrectly
counted as a user turn. After `isRealUserTurn()` excluded slash commands,
the turn list is [ALPHA1, BETA2, GAMMA3] and Up from the initial
selection (GAMMA3) lands on BETA2. Update the assertion to match.

Ref: https://github.com/QwenLM/qwen-code/pull/3441#issuecomment-4319798259

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-26 06:49:42 +08:00
qqqys
83d1e6dcae
feat: adds a Space-to-preview affordance to the /resume session picker (#3605)
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 Space-to-preview in resume session picker

Press Space on a highlighted session to open a read-only transcript
preview; Enter resumes, Esc returns. Works from both in-session
`/resume` and standalone `qwen --resume`.

The standalone path runs before `loadCliConfig`, so no real Config /
LoadedSettings exist when its render tree mounts. `StandaloneSessionPicker`
wraps the picker in stub Providers — every downstream access in the
preview render path is either optional-chained or gated on states
(Confirming / Executing) that never occur in resumed session data, so
the stubs' methods are only read, never invoked for real work. Tool
descriptions degrade to the raw function-call name in preview; users
get full fidelity after pressing Enter to resume.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(cli): guard SessionPreview separator width on narrow terminals

`'─'.repeat(boxWidth - 2)` would throw RangeError when columns < 6
(tmux splits, small panes). Clamp boxWidth to a safe minimum and
compute separatorWidth with Math.max(0, …).

Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>

* fix(cli): gate Space-to-preview behind enablePreview prop

`SessionPicker` is shared by the resume dialog and the delete-session
dialog. Preview's Enter shortcut forwards to `onSelect`, which for
delete is `handleDelete` — so Space → preview → Enter would silently
delete the session while the preview UI still says "Enter to resume".

Add `enablePreview?: boolean` (default false). Resume callers (the
in-app resume dialog and `--resume` standalone) opt in; the delete
dialog stays opt-out and behaves exactly as before. Footer hint and
preview render branch are both gated on the prop. Add a regression
test that emulates the delete dialog and asserts Space is a no-op,
the hint is absent, and Enter still flows straight to onSelect.

Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <noreply@qwen.ai>
Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
2026-04-25 22:41:03 +08:00
Reid
70cebc46a8
test(arena): cover select dialog key actions (#3614)
Add ArenaSelectDialog tests for Escape, discard, and winner selection key paths.

  Verify Escape only closes the dialog, x discards without applying changes, Enter applies the highlighted successful agent, and
  failed agents remain inert when selected.
2026-04-25 22:30:11 +08:00
易良
b127258328
fix(review): respect /language output setting for local reviews (#3611)
The /review skill's language rule "match the language of the PR" has no
applicable target during local reviews (no PR exists). When a user sets
an output language via /language, local review output now honors that
preference instead of defaulting to English.

PR reviews remain unchanged — they continue matching the PR's language
since findings may be published as inline comments visible to all
collaborators.

Closes #3594
2026-04-25 22:27:30 +08:00
jinye
c406c73509
feat(cli): add conversation rewind feature with double-ESC and /rewind command (#3441)
* feat(cli): add conversation rewind feature with double-ESC and /rewind command (#3186)

Add the ability to rewind conversation to a previous user turn, similar
to Claude Code's message selector. Users can trigger rewind via:
- Double-ESC on empty prompt while idle
- /rewind (or /rollback) slash command

The RewindSelector component provides a two-phase UI: a scrollable
pick-list of user turns followed by a confirmation dialog. On confirm,
both UI history and API history are truncated consistently, the terminal
is re-rendered, and the original prompt text is pre-populated in the
input for editing.

Key implementation details:
- historyMapping.ts correctly handles tool-call loops (functionResponse
  entries) and the startup context pair when mapping UI turns to API
  Content[] indices
- useDoublePress hook provides generic double-press detection with
  800ms timeout and proper cleanup on unmount
- ESC handler guards against WaitingForConfirmation state to prevent
  accidental rewind during tool approval
- Chat recording service records rewind events with tree-branching
  via parentUuid for session replay support

Closes #3186

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

* fix: call recordRewind() in handleRewindConfirm and simplify payload

- Actually invoke chatRecordingService.recordRewind() after rewind
- Remove tree-branching from recordRewind (no UI-to-recording UUID
  mapping exists yet) to avoid corrupting the parentUuid chain
- Simplify RewindRecordPayload to just truncatedCount

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

* test: add tmux-based E2E script for rewind feature

Automated verification of all 5 manual test items from PR description:
  1. /rewind command flow (pick turn, confirm, verify truncation)
  2. Double-ESC opens selector (with btw dismiss handling)
  3. ESC during streaming cancels (no rewind)
  4. /rewind with no history (guard blocks)
  5. After rewind, model ignores removed turns

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

* fix(rewind): resolve resume persistence and IDE mode issues

- chatRecordingService: add turnParentUuids tracking and rewindRecording()
  which re-roots the parentUuid chain so rewound messages land on a dead
  branch; reconstructHistory() then skips them automatically on resume.
  Add rebuildTurnBoundaries() for re-populating the index after /resume.
- AppContainer: fix truncatedCount bug (was always 0 after loadHistory),
  wire handleRewindConfirm to rewindRecording() with correct targetTurnIndex,
  add config.getIdeMode() guard to openRewindSelector so rewind is disabled
  in IDE sessions where extra user Content entries break the API boundary
  mapping.
- useResumeCommand: call rebuildTurnBoundaries() after startNewSession so
  rewind works correctly within resumed sessions.
- resumeHistoryUtils: surface "Conversation rewound." info item when a
  rewind record is encountered during history reconstruction.
- historyMapping.test.ts: add 9 unit tests for computeApiTruncationIndex
  covering normal flow, startup context pair, tool responses, and
  compression fallback.
- Copyright headers: standardize new files to "Copyright 2025 Qwen Code".

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(rewind): close slash-command, compression, and IDE bypass holes

Three bugs found by Codex review:

1. P1: `/rewind` slash command bypassed the IDE-mode guard because
   `slashCommandActions.openRewindSelector` called `setIsRewindSelectorOpen`
   directly. Fixed by introducing a ref bridge (`openRewindSelectorRef`)
   that delegates to the guarded callback.

2. P1: Slash-command invocations (`/help`, `/stats`, etc.) are stored as
   `type: 'user'` in UI history but never reach the API or recording
   service. The turn-index counter in `handleRewindConfirm` and
   `computeApiTruncationIndex` counted them, producing off-by-N errors.
   Added `isRealUserTurn()` helper that excludes items starting with
   `/` or `?`, applied in all three counting sites (AppContainer,
   historyMapping, RewindSelector).

3. P2: After chat compression, `computeApiTruncationIndex` returned
   `apiHistory.length` when the target turn was unreachable, silently
   keeping the full API history while the UI was truncated. Changed to
   return `-1`; `handleRewindConfirm` now aborts with an error message
   when the target turn was absorbed by compression.

Tests: 14 unit tests for historyMapping (including slash-command and
compression cases), full suite 616/616 passed.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-25 22:12:29 +08:00
Shaojin Wen
e937d15b90
fix(cli): drain runExitCleanup before process.exit in error handlers (#3602)
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:none (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
handleError / handleCancellationError / handleMaxTurnsExceededError all
called process.exit synchronously, bypassing the caller's runExitCleanup
-> Config.shutdown -> chat-recording flush() chain on SIGINT, max-turn,
and fatal-error paths. Same family as the EPIPE bypass fixed in
bf24fff1f, just on a different code path.

Makes the three handlers async and routes the exit through a shared
exitAfterCleanup() helper that awaits runExitCleanup() before the actual
process.exit. The helper carries an exit-once latch so a SIGINT racing a
stream rejection (handleCancellationError + handleError fired
concurrently) doesn't end up running cleanup twice or interleaving exit
calls — only the first caller drains and exits, the second parks on a
never-resolving promise that's killed when process.exit fires.

Text-mode handleError still throws to the caller (unchanged behavior),
but now drains the queue first so the unhandled-rejection path doesn't
lose chat-recording records.

Five call sites in nonInteractiveCli.ts updated to await. Existing 11
errors.test.ts cases adapted to async + rejects.toThrow; added 5 new
regression guards covering cleanup-before-exit ordering for each
handler plus the concurrent-handler race.

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-25 11:07:30 +08:00
ChiGao
54465b0c02
fix(cli): add TUI flicker foundation fixes (#3591)
* fix(cli): reduce main screen flicker

* fix(cli): pre-slice large tool text output

* fix(cli): slice tool output by visual height

* fix(core): preserve shell transcript across narrow wraps

* fix(core): suppress soft-wrap-only shell rerenders

* fix(core): compare default shell output by logical wraps

* fix(cli): gate synchronized terminal output

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-04-25 10:13:34 +08:00
John London
007a109db8
fix(cli): respect OPENAI_MODEL precedence in CLI model resolution (#3567)
Some checks failed
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:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* fix(cli): respect OPENAI_MODEL precedence in CLI model resolution

* test(cli): cover env-driven model precedence for OpenAI-compatible auth

* fix(cli): scope model env precedence by auth type

* test(cli): cover QWEN_MODEL fallback precedence
2026-04-25 07:19:27 +08:00
jinye
e384338145
feat(SDK) Add Python SDK implementation for #3010 (#3494)
* Codex worktree snapshot: startup-cleanup

Co-authored-by: Codex

* Add Python SDK real smoke test

Adds a repository-only real E2E smoke script for the Python SDK, plus npm and developer documentation entry points.

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

* fix(sdk-python): address review findings — bugs, type safety, and test coverage

- Fix prepare_spawn_info: JS files now use "node" instead of sys.executable
- Fix protocol.py: correct total=False misuse on 7 TypedDicts (required fields were optional)
- Fix query.py: add _closed guard in _ensure_started, suppress exceptions in close()
- Fix sync_query.py: prevent close() deadlock, add context manager, add timeouts
- Fix transport.py: handle malformed JSON lines, add _closed guard in start()
- Fix validation.py: use uuid.RFC_4122 instead of magic UUID
- Fix __init__.py: export TextBlock, widen query_sync signature
- Remove dead code: ensure_not_aborted, write_json_line, _thread_error
- Add 12 new tests (29 → 41): context managers, JSON skip, closed guards, spawn info, timeouts

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

* fix(sdk-python): address wenshao review — session_id, bool validation, debug stderr

- Fix continue_session=True generating a wrong random session_id
- Add _as_optional_bool helper for strict type validation on bool fields
- Default debug stderr to sys.stderr when no custom callback is provided

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

* fix(sdk-python): address remaining wenshao review feedback

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

* test(cli): harden settings dialog restart prompt test

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

* fix(sdk-python): review fixes — UUID compat, stderr fallback, sync cleanup

- Remove UUID version restriction to support v6/v7/v8 (RFC 9562)
- Always write to sys.stderr when stderr callback raises (was silent when debug=False)
- Prevent duplicate _STOP sentinel in SyncQuery.close() via _stop_sent flag
- Add ruff format --check to CI workflow
- Fix smoke_real.py version guard: fail early before imports instead of NameError
- Apply ruff format to existing files

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

* fix(sdk-python): remaining review fixes — exit_code attr, guard strictness, sync timeout

- Add exit_code attribute to ProcessExitError for programmatic access
- Strengthen is_control_response/is_control_cancel guards to require
  payload fields, preventing misrouting of malformed messages
- Expose control_request_timeout property on Query so SyncQuery uses
  the configured timeout instead of a hardcoded 30s default
- Use dataclasses.replace() instead of direct mutation on frozen-style
  QueryOptions in query() factory
- Add ResourceWarning in SyncQuery.__del__ when not properly closed

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

* fix(sdk-python): add exit_code default and guard __del__ against partial GC

- Give ProcessExitError.exit_code a default value (-1) so user code can
  construct the exception with just a message string
- Wrap SyncQuery.__del__ in try/except AttributeError to prevent crashes
  when the object is partially garbage-collected

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

* fix(sdk-python): review fixes — resource leak, type safety, CI matrix, docs

- Fix SyncQuery.__del__ to call close() on GC instead of only warning
- Replace hasattr duck-type check with isinstance(prompt, AsyncIterable)
- Type-validate permission_mode/auth_type in QueryOptions.from_mapping
- Use TypeGuard return types on all is_sdk_*/is_control_* predicates
- Add 5s margin to sync wrapper timeouts to prevent error type masking
- Expand CI matrix to test Python 3.10, 3.11, 3.12
- Change ProcessExitError.exit_code default from -1 to None
- Add stderr to docs QueryOptions listing
- Update README sync example to use context manager pattern

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

* fix(sdk-python): preserve iterator exhaustion state and suppress detached task warning

- Add _exhausted flag to Query.__anext__ and SyncQuery.__next__ so
  repeated iteration after end-of-stream raises Stop(Async)Iteration
  instead of blocking forever.
- Remove re-raise in _initialize() to prevent asyncio
  "Task exception was never retrieved" warning on detached tasks;
  the error is already surfaced via _finish_with_error().

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

* fix(sdk-python): reject mcp_servers at validation time and add iterator/init tests

- Reject mcp_servers in validate_query_options() with a clear error
  instead of advertising MCP support to the CLI and then failing at
  runtime when mcp_message arrives.
- Remove dead mcp_servers branch from _initialize().
- Add tests for async/sync iterator exhaustion, detached init task
  warning suppression, and mcp_servers validation.

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

* fix(sdk-python): fix ruff lint errors in new tests

- Use ControlRequestTimeoutError instead of bare Exception (B017)
- Fix import sorting for stdlib vs third-party (I001)
- Break long line to stay within 88-char limit (E501)

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

* style(sdk-python): apply ruff format to new tests

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

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-25 07:02:58 +08:00
易良
202be6ec7d
feat(vscode): expose /skills as slash command with secondary picker (#2548)
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:none (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(vscode): expose /skills as slash command with secondary picker

Add a secondary completion picker for the /skills slash command in the
VSCode IDE companion, allowing users to browse and select skills from
a dropdown before sending.

Changes:
- CLI: add 'skills' to ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE whitelist
- CLI: send available_skills_update via ACP with skill names/descriptions
- Extension: handle available_skills_update in session update handler
- Webview: implement secondary picker that triggers after selecting /skills
- Webview: allow spaces in completion trigger for /skills sub-queries

Closes #1562

Made-with: Cursor

* feat(vscode-ide-companion): embed skills in commands update metadata

- Move available skills from separate session update to _meta field of
  available_commands_update for more efficient delivery
- Simplify skill data to just skill names (string array)
- Add skillsCompletion utility for secondary picker logic
- Cache available skills in WebViewProvider for replay on webview ready
- Update all related types and handlers to support the new structure

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

* refactor(vscode-ide-companion): simplify skills picker flow

* refactor(vscode-ide-companion): extract skills completion utils to shared module

Move `isSkillsSecondaryQuery`, `shouldOpenSkillsSecondaryPicker`, and
`SKILL_ITEM_ID_PREFIX` from App.tsx and useCompletionTrigger.ts into a
shared `completionUtils.ts` file to eliminate duplication.

* fix(vscode-ide-companion): restore skills picker state on reload

Cache and replay available skills when the webview becomes ready again.

Clear stale skills when commands metadata does not include availableSkills.

* fix(vscode-ide-companion): replay slash commands after webview reload

Cache available commands in the webview provider.

Replay them on webviewReady so slash command state survives reloads.

* fix(vscode-ide-companion): import AvailableCommand from ACP SDK

* fix(vscode-ide-companion): fallback /skills to direct command

* test(vscode-ide-companion): cover skills secondary picker flow

* test(vscode-ide-companion): guard App mock initialization

* fix(vscode-ide-companion): remove duplicate AvailableCommand import

The auto-merge introduced a duplicate AvailableCommand in the
@agentclientprotocol/sdk import block, causing TS2300.

* fix(vscode-ide-companion): remove duplicate availableCommands replay in handleWebviewReady

The handleWebviewReady method was sending cachedAvailableCommands twice
on every webview-ready handshake, causing an unnecessary extra state
update in the webview.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-24 23:28:53 +08:00
qqqys
3a2ee4ac1d
fix(cli): memoize useHistory() return to avoid unnecessary re-renders (#3547) 2026-04-24 22:57:47 +08:00
pomelo
55c63e1a2a
docs(github): tighten PR template validation guidance (#3522)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-24 22:14:06 +08:00
MikeWang0316tw
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>
2026-04-24 21:34:46 +08:00
Shaojin Wen
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 d6485964c.

Follow-up flagged by zhangxy-zju on #3581.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-24 21:17:51 +08:00
dreamWB
5e4ff3755c
feat(vscode): add native context menu copy actions for webview chat (#3477)
* feat(vscode): add native context menu copy actions for webview chat

Add three right-click context menu items to the chat message area using
VSCode's native webview/context API:
- Copy Message: copies the right-clicked message's raw markdown content
- Copy All Messages: copies the full conversation in markdown format
- Copy Last Reply: copies the last assistant response

Implementation details:
- Commands registered in package.json with webview/context menu entries
- Clipboard writes go through extension host (vscode.env.clipboard) for
  reliability in webview sandbox
- Message identification via data-msg-idx stamped after render
- Tool-call outputs supported including diff format (git diff style)
- i18n support via package.nls.json (English) and package.nls.zh-cn.json
- Menu only shown in message area (not input box or empty state)

Closes #3052

* fix(vscode): wrap tool-call content text in code blocks for copy

* fix(vscode): only wrap tool-call content in code blocks for Copy All, not single Copy Message

* fix(vscode): route copy commands to the right-clicked webview and use dynamic code fences

* fix(vscode): use childIndexMap for copy-message routing and extract shared message handling

Replace the wrapper-div approach (which broke CSS layout) with a
render-time childIndexMap that maps DOM child positions to allMessages
indices. This avoids both the useLayoutEffect index-drift bug and the
wrapper-div CSS side effects.

- Remove data-msg-idx wrapper divs; messages render directly as
  container children, preserving original [&>*] CSS layout
- Build childIndexMap during MessageList render, skipping null items
  (empty AssistantMessage, hidden tool calls via shouldShowToolCall)
- findMessageIndex walks up from click target to container's direct
  child, then maps through childIndexMap
- Filter hidden tool calls and empty content in copyAllMessages
- Extract handleCommonWebviewMessage to deduplicate routing logic
  across sidebar, editor panel, and restored panel handlers
- Clear lastContextMenuProvider on dispose to prevent memory leaks

* fix(vscode): handle image messages in copy and resolve intermittent copy failure

- Copy Message on image messages now outputs markdown format ![image](path)
  instead of empty string
- Copy All Messages includes image messages as ![image](path) instead of
  skipping them
- Copy Last Reply skips empty assistant placeholders during streaming
- Resolve intermittent copy failure by pre-resolving message index on
  right-click instead of storing a DOM element reference that can become
  stale after React re-renders
2026-04-24 20:26:56 +08:00
易良
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
2026-04-24 19:55:12 +08:00
易良
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
2026-04-24 17:55:26 +08:00
Fu Yuchen
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)
2026-04-24 17:49:05 +08:00
tanzhenxin
e47b22806b
feat(docs): add qwen-code skills, agents, and updated AGENTS.md (#3575)
Some checks are pending
Qwen Code CI / CodeQL (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
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
- Add new skills: bugfix, feat-dev with structured workflows
- Update existing skills: docs-audit-and-refresh, docs-update-from-diff,
  e2e-testing, qwen-code-claw, structured-debugging, terminal-capture
- Update test-engineer agent with clearer constraints and formatting
- Update qc commands: bugfix, code-review, commit, create-issue, create-pr
- Reorganize .gitignore to keep qwen configs near top
- Expand AGENTS.md with development commands, feature/bugfix workflows,
  project directories table, and code review guidelines

Co-authored-by: 愚远 <zhenxing.tzx@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-24 17:33:03 +08:00
qqqys
2815a2fcd7
revert(vscode-ide-companion): undo #3450 split-stream timestamp sharing (#3573)
#3450 pinned every assistant/thinking segment in a streamed turn to the
same turn-start timestamp so a later user message could not be sorted
between two segments of the previous turn (#3273). That fix turned out
to conflict with the tool-call timeline: tool calls carry their own
arrival timestamp, which is strictly greater than the turn-start
timestamp, so after #3450 every tool call sorted AFTER both assistant
segments instead of between them — the exact 'tool call jumped to the
end' ordering bug users are now reporting.

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

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

Refs: #3273, #3450

Co-authored-by: Qwen-Coder <noreply@qwenlm.ai>
2026-04-24 17:13:38 +08:00
tanzhenxin
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>
2026-04-24 17:11:00 +08:00
顾盼
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
2026-04-24 15:27:55 +08:00
zhangxy-zju
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 (4519c5f9c) interpreted the CI symptom as
"10 microtask yields aren't enough" and rewrote the assertion around a
deferred `Promise.all`. But the old test's `toHaveBeenCalledTimes(1)`
failure with 0 invocations was already the same bug — execute was never
called. The new formulation just converted the visible failure from an
assertion mismatch into a timeout.

Switch the guard to a truthy check against `invocation.eventEmitter`.
Semantics for real AgentTool are unchanged — `agent.ts:392` declares
`readonly eventEmitter: AgentEventEmitter = new AgentEventEmitter()`,
so production always enters the branch. The only new behavior is that
incomplete invocations (or test mocks) skip SubAgentTracker setup
cleanly instead of crashing. `subAgentCleanupFunctions` stays `[]`,
so the cleanup forEach at the success/error paths is a no-op.
2026-04-24 15:22:45 +08:00
顾盼
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
2026-04-24 14:53:01 +08:00
harsh
5c1e636dbe
fix: Strengthen error handling in qwenOAuth2.ts to prevent unhandled 'error' event (#3481)
* fix: strengthen error handling in launchBrowser to prevent unhandled events

* fix: strengthen error handling with ChildProcess type and debugLogger

* fix: use type-only import for ChildProcess
2026-04-24 14:43:13 +08:00
tanzhenxin
53293e4d85
refactor(core): make OpenAI converter stateless (follow-up to #3525) (#3550)
* refactor(core): make OpenAI converter stateless to prevent shared-state races

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

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

* test(core): align timeout expectations
2026-04-24 12:28:03 +08:00
tanzhenxin
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.
2026-04-24 12:27:38 +08:00
顾盼
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
2026-04-24 11:29:02 +08:00
chaoliang yan
3e74a33460
fix(sdk-java): pass custom env to CLI process (#3543)
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
2026-04-24 10:37:52 +08:00
Edenman
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.
2026-04-24 09:50:20 +08:00
jinye
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>
2026-04-24 00:38:32 +08:00
Shaojin Wen
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.
2026-04-23 20:37:05 +08:00
zhangxy-zju
d14ce16b95
fix(core): treat empty 'pages' parameter as unset in ReadFile (#3559)
params.pages !== undefined let "" fall through to parsePDFPageRange(''),
which returns null and surfaced "Invalid pages parameter: ''" for every
read_file call from models that default optional strings to "".

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

Fixes #3558
2026-04-23 20:02:05 +08:00
顾盼
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>
2026-04-23 11:06:07 +08:00
顾盼
78037d996b
fix(cli): stabilize resume callback deps (#3533) 2026-04-23 10:31:35 +08:00
Shaojin Wen
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)
2026-04-23 08:52:37 +08:00
zhangxy-zju
d40fe7cdba
fix(core): scope StreamingToolCallParser per stream, not per Converter (#3516) (#3525)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(core): scope StreamingToolCallParser per stream, not per Converter

Issue #3516 reports subagent failures with `Model stream ended with
empty response text` whose real root cause is concurrent streams
racing on a single shared tool-call parser.

Architecture before this change:

    Config (singleton)
      └── contentGenerator (OpenAIContentGenerator)
            └── ContentGenerationPipeline
                  └── OpenAIContentConverter
                        └── streamingToolCallParser  ← shared!

Any caller of `Config.getContentGenerator()` — foreground turns,
fork subagents, `run_in_background: true` subagents, ACP concurrent
Agent calls (PR #3463) — ends up using the same parser instance.
When two streams run concurrently, `processStreamWithLogging`'s
stream-start `resetStreamingToolCalls()` wipes the other stream's
in-flight buffers, and their chunks interleave at `index: 0`,
producing corrupt JSON like
`{"file_path": "/A{"file_path": "/B...` that even jsonrepair cannot
salvage. The corrupted tool calls are dropped entirely and the
stream surfaces upstream as `NO_RESPONSE_TEXT`.

Fix: move parser state from Converter instance field into
per-stream local state.

- Add `ConverterStreamContext` and `createStreamContext()` factory
  on `OpenAIContentConverter`. Each call returns a fresh context
  holding its own `StreamingToolCallParser`.
- `convertOpenAIChunkToGemini(chunk, ctx)` now takes the context
  as an explicit arg; all internal parser calls route through it.
- `ContentGenerationPipeline.processStreamWithLogging` creates one
  context at stream entry and passes it to every chunk conversion.
- Drop `OpenAIContentConverter.streamingToolCallParser` field.
- Drop `resetStreamingToolCalls()` — the context has stream-local
  lifetime, no manual reset needed. The two call sites in the
  pipeline (stream entry and error path) are removed.

Tests:

- Replace the `resetStreamingToolCalls` suite with a
  `createStreamContext` suite asserting that distinct contexts are
  independent and writes to one never leak into the other.
- Add a regression test simulating two concurrent streams with
  interleaved chunks through the same Converter instance; both
  tool calls close cleanly with correct arguments and ids.
- All existing single-stream tests updated to obtain a context via
  `createStreamContext()` and pass it through to chunk conversion.
- `pipeline.test.ts` mocks updated accordingly.

packages/core test suite: 841 passed. No stale references to
`resetStreamingToolCalls` or the private parser field remain.

Refs #3516

* docs(core): clarify GC wording in per-stream context comment (copilot review)

* test(core): add pipeline-level integration test for concurrent streams

Complements the unit tests in converter.test.ts by driving the real
ContentGenerationPipeline + real OpenAIContentConverter (no mocks on
converter) through two streams that interleave on the event loop via
`setImmediate`-paced async generators.

Two scenarios:

1. Happy path — two concurrent executeStream invocations with their
   own tool-call chunks. Assert each stream emits its own function
   call with the correct id and args (not cross-contaminated from
   the sibling stream).

2. Error isolation — one stream hits `error_finish` mid-flight while
   a sibling stream is still accumulating tool-call chunks. Assert
   the sibling's function call still emits cleanly, covering the
   removed `resetStreamingToolCalls()` call in the error path of
   processStreamWithLogging.

Verified as a positive control: with the per-stream context fix
reverted (origin/main state), both tests fail with exactly the
bug shape users reported — one stream's function call is either
overwritten by the other's id/args, or is swallowed entirely when
the sibling stream's error path wipes the shared parser buffer.

Refs #3516
2026-04-22 20:32:30 +08:00
易良
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.
2026-04-22 19:26:13 +08:00
顾盼
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).
2026-04-22 19:12:44 +08:00
Edenman
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.
2026-04-22 16:58:45 +08:00
jinye
685296e978
fix(core): reject truncated subagent write_file calls (#3505)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(core): reject truncated subagent write_file calls

Propagate MAX_TOKENS truncation from subagent responses into tool requests and reject truncated edit calls before schema validation can surface misleading missing-parameter errors.

* fix(core): reset per-attempt stream state on retry in agent-core

When a streaming response hits MAX_TOKENS and is retried, accumulated
state variables (functionCalls, wasOutputTruncated, roundText, etc.)
were not cleared. This caused a successful retry to inherit the stale
wasOutputTruncated=true flag from the failed attempt, incorrectly
rejecting all Edit tool calls with a truncation error.

Reset all per-attempt state on retry events, matching the existing
behaviour in turn.ts. Add a regression test covering the
truncated-then-retried-successfully scenario.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-22 15:01:42 +08:00
zhangxy-zju
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.
2026-04-22 14:46:33 +08:00
Shaojin Wen
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
2026-04-22 14:37:13 +08:00
qqqys
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>
2026-04-22 11:48:01 +08:00