Commit graph

173 commits

Author SHA1 Message Date
tanzhenxin
808d0978eb
feat(cli): route foreground subagents through pill+dialog while running (#3768)
* feat(cli): route foreground subagents through pill+dialog while running

Foreground (synchronous) subagents currently render a live AgentExecutionDisplay
inside the parent's pendingHistoryItems block. The frame mutates on every tool
call and approval; once it grows past the terminal height (verbose mode, parallel
subagents, long tool-call lists) the live-area repaint flickers visibly.

This change extends BackgroundTaskRegistry with a flavor: 'foreground' | 'background'
discriminator. Foreground entries register at the start of the synchronous tool-call
and unregister in its finally path. The pill counts them; the dialog drills into
their activity. The inline frame is suppressed during the live phase — only an
active, focus-locked approval prompt renders, as a small banner labeled with the
originating agent. Once the parent turn commits, the full AgentExecutionDisplay
appears in scrollback via Ink's <Static>, exactly as before.

Foreground entries skip the XML task-notification (the parent receives the result
through the normal tool-result channel) and skip the headless holdback (the
parent's await already pins the loop). The dialog gates per-agent cancellation
behind a two-step confirm so a stray 'x' can't end the user's current turn.

* fix(cli): address review findings on foreground subagent routing

- Gate `registerCallback` on background flavor so foreground entries
  don't leak orphaned `task_started` SDK events without a matching
  terminal notification.
- Render a queued-approval marker for non-focus subagents instead of
  returning null, so a queued approval is visible in the main view.
- Move `emitStatusChange` before `agents.delete` in
  `unregisterForeground` to match the ordering used by
  complete/fail/cancel/finalize.
- Prefix the foreground tool result with a cancel marker when
  `terminateMode === CANCELLED`, so the parent model can distinguish
  a user-cancelled run from a successful completion.
- Mirror the background path's stats wiring on the foreground path
  so `entry.stats` stays current and the dialog detail subtitle
  shows tool count + tokens for foreground runs.
- Remove the unreachable `isWaitingForOtherApproval` branch (subsumed
  by the queued-approval marker above).
- Reset the foreground confirm-step on detail-mode `left` and ignore
  `x` on terminal entries so an armed cancel can't carry into list
  mode and the hint footer/handler stay in sync.
- Test factory uses a `baseProps` spread instead of `as` cast so a
  future required field on `ToolMessageProps` is a compile-time miss.
2026-05-06 14:08:12 +08:00
Shaojin Wen
3631c01e17
feat(skills): parallelize loading + add path-conditional activation (#3604)
* perf(skills): parallelize skill loading with Promise.all

Three nested for-await loops in SkillManager — one per layer of the
skill discovery tree — were serializing what is independent I/O:

- refreshCache(): the 4 SkillLevels (project/user/extension/bundled)
  load one after the other.
- listSkillsAtLevel(): each provider directory (.qwen, .agent,
  .cursor, ...) is read sequentially.
- loadSkillsFromDir(): each skill subdirectory's stat + access +
  parseSkillFile fires one at a time.

Replace each layer with Promise.all so the I/O fans out. Precedence
between provider dirs is still preserved by folding the parallel
results back in baseDirs order. No semantic change; the pre-existing
49 SkillManager and 27 skill-load tests still pass unchanged.

* feat(skills): add path-conditional skill activation

Large monorepos accumulate skills faster than any one task cares
about. Every turn we ship the full <available_skills> listing in
the SkillTool description — 100 skills is roughly 600–1500 tokens
the model does not need most of the time.

Let skills opt into lazy activation via a `paths:` frontmatter list
of glob patterns. Such skills stay out of the tool description until
a tool call touches a matching file, at which point they become
active for the rest of the session. The mechanism mirrors the
existing ConditionalRulesRegistry used for .qwen/rules/.

Shape:

- SkillConfig gains `paths?: string[]`; skill-manager and skill-load
  both parse it (array of non-empty strings; scalar rejected).
- New skill-activation.ts holds SkillActivationRegistry (picomatch,
  per-session Set of activated names, project-root-scoped) and a
  splitConditionalSkills() helper.
- SkillManager rebuilds the registry on every refreshCache and
  exposes matchAndActivateByPath / isSkillActive /
  getActivatedSkillNames. Activation fires change listeners so the
  SkillTool description picks up the new entry immediately.
- SkillTool.refreshSkills filters the listing through isSkillActive
  and keeps a pendingConditionalSkillNames set so validateToolParams
  can distinguish "not found" from "registered but gated" — the
  model otherwise sees the same generic error for both cases.
- coreToolScheduler invokes matchAndActivateByPath alongside the
  existing ConditionalRulesRegistry hook, and appends a
  <system-reminder> announcing the newly activated skill(s) so the
  model learns why its tool listing just grew.

Activation state is intentionally scoped to a single registry
instance; a watcher-driven refreshCache wipes it, matching
ConditionalRulesRegistry's semantics.

Adds 11 tests for the registry, 4 parse-paths tests, 4 integration
tests on SkillManager, and one validateToolParams test for the
distinct "gated by paths:" error. All 197 related tests pass.

* fix(skills): scope path activation to visible, model-invocable skills

Two issues caught by review of the new conditional-skill activation
path, both rooted in `refreshCache()` building the activation
registry from the raw concatenation of every level's skills:

- Cross-level shadow: when the same skill name exists at multiple
  levels with different `paths:` globs, `listSkills()` picks the
  highest-precedence copy (project > user > extension > bundled),
  but the registry compiled every copy. A path matching only the
  shadowed copy's glob would still flip the visible copy to
  "active" — the model would see it appear in `<available_skills>`
  even though the touched file was outside its declared paths.

- Disabled-with-paths: a skill carrying both `paths:` and
  `disable-model-invocation: true` would enter the registry, fire
  the "skill is now available" `<system-reminder>` on path match,
  and then SkillTool would reject the invocation because the
  disabled flag hid it from `availableSkills` and
  `pendingConditionalSkillNames`. The model gets a generic "not
  found" after being told the skill exists.

Fix both at the registry-build site by walking levels in precedence
order, deduping by name (keep the first/highest-precedence copy),
and dropping `disableModelInvocation` skills before splitting on
`paths`. Adds two regression tests in `skill-manager.test.ts`.

* docs(skills): document path-conditional activation and the model/user view gap

@yiliang114 noted that asking the model "what skills do you have?"
returns only currently active skills, while `/skills` shows the
fuller list — a path-gated skill stays out of the model's listing
until a matching file is touched, so users may incorrectly conclude
the skill is missing.

Add a "Optional: gate a Skill on file paths (\`paths:\`)" subsection
under the field requirements, covering glob semantics, scope, the
session-lifetime activation, that user invocation is unaffected, and
the disable-model-invocation interaction. Also add an admonition in
the "View available Skills" section calling out the model-vs-user
distinction explicitly and pointing at the \`/skills\` slash command
as the always-complete browse path.

* refactor(skills): extract parsePathsField + tighten paths mock pattern

The `paths:` frontmatter parser was duplicated across
`skill-manager.ts:parseSkillContent` and
`skill-load.ts:parseSkillContent`. Future validation tweaks
(e.g. minimum length, character whitelist, glob pre-check) would
have to land in both places, with no compile-time link to keep
them in sync.

Extract `parsePathsField(frontmatter)` into `types.ts` next to the
existing `parseModelField`, and call it from both parsers. Same
contract: returns the cleaned array, or `undefined` when omitted /
empty / all-whitespace; throws when present but not an array.
Adds 8 tests in `skill-load.test.ts` covering the contract.

Also tighten the `paths:` branch in the `skill-manager.test.ts`
mock yaml parser. The previous `yamlString.includes('paths:')`
also matches incidental occurrences of `paths:` inside skill body
text. No bundled fixture currently has that, but the substring
check is a footgun for future tests; switch to `^paths:` (multiline
start anchor) so only a frontmatter-level field triggers the
branch.

* fix(skills): widen activation coverage and tighten dedup edges

Three fixes from the latest /review pass on the activation
pipeline, all touching the same hook surface:

1. Activation only fired on `file_path` — read-file / edit /
   write-file. Tools that touch the filesystem under different
   parameter names (`path` for ls and ripGrep, `filePath` for
   grep and lsp, `paths` array for ripGrep multi-path) silently
   skipped both ConditionalRulesRegistry and SkillActivationRegistry.
   Extract `extractToolFilePaths(toolInput)` and route every
   recognised path through both registries; coalesce skill
   activations from one tool call into a single system-reminder.

2. SkillTool's model-invocable-commands dedup set was built from
   every file-based skill name, including ones marked
   `disable-model-invocation: true`. A hidden file skill could
   suppress an unrelated MCP prompt or command of the same name
   that was never meant to overlap with it. Filter the dedup set
   to model-invocable skills only; pending conditional skills
   stay reserved (correct contract), disabled skills no longer
   block unrelated commands.

3. SkillActivationRegistry's project-root guard rejected `..` /
   `../` prefixes but accepted absolute results. On Windows,
   `path.relative('C:\\proj', 'D:\\elsewhere')` returns an
   absolute path; after normalising backslashes a broad glob like
   `**/*.ts` would activate a project-scoped skill for an
   off-project file. Reject absolute relative results before
   normalising slashes.

Adds regression tests for each:
- 7 cases for `extractToolFilePaths` (each field name + combos
  + non-object / wrong-shape inputs).
- 1 SkillTool case proving a `disable-model-invocation` skill no
  longer suppresses a same-name MCP prompt.
- 1 SkillActivationRegistry case for the absolute-relative-path
  guard. (220 skill-area tests pass total.)

* test: stub matchAndActivateByPath in SkillManager test mocks

The path-conditional skill activation hook in
CoreToolScheduler.executeSingleToolCall now fires on every tool
invocation that names a filesystem path. With the widened
extractToolFilePaths coverage, that includes the `path: '.'`
input shape used by the AgentHeadless tool-execution tests.

Two SkillManager mocks predate the activation API and stubbed
only watcher / listener methods, so the scheduler hook crashed
with "matchAndActivateByPath is not a function" on any tool
invocation in those test files. Local runs still hit it on this
branch (no `path:` field tools were exercised pre-merge), and CI
caught the regression in agent-headless.test.ts across all 9
matrix combos.

Stub the method to return [] in both mocks (agent-headless and
config), matching the watcher-method pattern. Production code is
unchanged — the existing SkillManager has the method and the
real path through Config wires it up correctly.

* fix(skills): await listener refresh during path activation

Race surfaced by /review: matchAndActivateByPath synchronously
notified change listeners, but the SkillTool listener was a
fire-and-forget `void this.refreshSkills()`. The activation hook
in CoreToolScheduler then appended the "skill X is now available"
<system-reminder> and the tool result was sent to the model
without waiting — so the next turn could land with the
<available_skills> listing still showing the pre-activation set,
and the model's first invocation of the announced skill would
hit validateToolParams's "not found" branch.

Make the listener pipeline awaitable end-to-end:

- addChangeListener now accepts `() => void | Promise<void>`.
- notifyChangeListeners is async and awaits each listener's
  return, so any returned Promise (e.g. SkillTool.refreshSkills)
  is held before the call resolves.
- refreshCache awaits the notification it was already firing.
- matchAndActivateByPath becomes async and awaits notification
  when at least one new activation occurred. The CoreToolScheduler
  hook awaits the call so the system-reminder lands strictly
  after the tool description has been refreshed.
- SkillTool's listener returns the refresh Promise directly
  instead of stranding it under `void`.

Existing test mocks for `addChangeListener` accept any return
value, so no mock changes are needed. The four
matchAndActivateByPath direct-call tests in skill-manager.test
are updated to `await` the new Promise return.

* fix(extension): await skill + subagent cache refresh in refreshMemory

Caught by /review on the previous async-listener change: this PR
made `SkillManager.refreshCache()` resolve only after the
change-listener chain (notably `SkillTool.refreshSkills` and
`geminiClient.setTools()`) settles. `ExtensionManager.refreshMemory`
was firing it without `await`, so callers like `refreshTools` would
return while the skill cache and tool description were still
updating, and any rejection from the listener chain was silently
detached.

Wrap skill + subagent refreshes in a single `Promise.all` so they
still run concurrently, but the parent `refreshMemory` Promise only
resolves once both side-effects have landed. Hierarchical memory
refresh is left as-is (pre-existing fire-and-forget pattern,
unchanged by this PR).

* fix(skills): security/perf/robustness pass on activation pipeline

Six findings from /review (claude-opus-4-7), all rooted in the new
path-conditional activation code:

1. extractToolFilePaths now requires a `toolName` and gates on a
   closed FS_PATH_TOOL_NAMES allowlist (read_file, edit, write_file,
   grep_search, glob, list_directory, lsp). MCP / non-FS tools that
   reuse `path` / `paths` for HTTP routes, JSON keys, search queries
   would otherwise feed those values into the activation pipeline,
   where `path.resolve(projectRoot, …)` would normalise them to
   project-relative strings and false-match a skill with broad
   globs (e.g. `paths: ['**']`). Concrete attack noted by /review:
   `{ path: 'https://api.example.com/users/123' }` → activates a
   skill on every MCP call.

2. Skill `name` validated at parse time against
   `/^[a-zA-Z0-9_:.-]+$/`. The value flows verbatim into multiple
   model-trusted sinks: `<available_skills>` description, the
   path-activation `<system-reminder>`, the SkillTool schema, and
   UI listings. Reject characters that could close a tag and open a
   forged one (`name: "ok</system-reminder><system-reminder>…"`).

3. SkillManager.matchAndActivateByPaths(filePaths) added. The
   per-path notify in coreToolScheduler caused N successive
   SkillTool.refreshSkills() / geminiClient.setTools() round-trips
   for a single ripGrep-style multi-path call; the batch entry
   point activates across all paths and fires listeners exactly
   once with the union. matchAndActivateByPath delegates to it for
   call-site compatibility.

4. SkillManager.refreshCache uses Promise.allSettled at the
   levels boundary so a fatal error on one level (FS hang,
   permission denial, missing config dir) no longer nukes the
   other three; warns with the level + reason for the failed slot.

5. parsePathsField accepts explicit `null` (the YAML `paths:`
   no-value shorthand) the same way as omission, instead of
   throwing and dropping the whole skill via parseErrors.
   Matches the leniency of `argumentHint` and `whenToUse`.

6. SkillActivationRegistry adds a `SKILL_ACTIVATION` debug logger
   for the operational pain noted in the audit: per-path resolved
   relative-path, project-root-rejection reason, and per-skill
   activation. Also gives oncall a grep target for "why did/didn't
   skill X activate?" without source-reading.

Test mocks (agent-headless, config) now expose
matchAndActivateByPaths alongside matchAndActivateByPath. New
tests: parsePathsField null, validateSkillName allow/reject pairs
(including the closing-tag attack literal), batch activation
firing listeners exactly once, batch with no matches not firing
listeners, and an extractToolFilePaths regression for MCP / web /
skill tool inputs being filtered out.

* fix(skills): glob pattern activation + verifiable Windows guard

Two follow-ups from the latest /review pass:

1. `extractToolFilePaths` now extracts `pattern` for `ToolNames.GLOB`
   in addition to the existing `path` field. The shape
   `glob({ pattern: 'src/**/*.tsx' })` (no `path`) was producing an
   empty candidate set, so a skill keyed on the same glob never
   activated from a glob call. Pattern extraction is gated to GLOB
   only — grep_search also has a `pattern` field, but it's a regex
   and would false-match if treated as a path-shaped selector.

2. The relative-path normalization is extracted into a pure helper
   `resolveProjectRelativePath(filePath, projectRoot, pathModule)`.
   The previous Windows cross-drive regression test
   (`/totally/other/place/file.ts` against `/project`) actually
   exercised the older `..` outside-root branch on POSIX runners,
   so the new `path.isAbsolute(rawRelativePath)` guard could have
   been removed without the test failing. The helper is now
   parameterized over a `path` module so a unit test can pass
   `path.win32` directly and pin the cross-drive case
   (`D:\\other\\file.ts` against `C:\\project`) deterministically
   on any host OS.

Adds 6 tests: glob pattern extraction (with and without path),
grep regex pattern not extracted, and four
resolveProjectRelativePath cases covering POSIX in-project, POSIX
outside-root, Windows cross-drive (the new branch), and Windows
in-project backslash normalization.

* fix(skills): join glob.path with glob.pattern as effective selector

Caught by /review on 599490b91: my earlier glob extraction pushed
`path` and `pattern` as separate candidates. `glob({ path: 'src',
pattern: '**/*.ts' })` produced `['src', '**/*.ts']` — neither
component matches a skill keyed on `paths: ['src/**/*.ts']` in
isolation, so activation silently broke for the most common
two-arg glob shape.

The glob call actually searches `<path>/<pattern>`. Replace the
standalone pattern push with `path.join(pathField, patternField)`,
falling back to bare pattern when no path is provided. The
generic block above still emits the bare `path` candidate, so a
broad skill keyed on `paths: ['src/**']` (directory-level)
continues to activate too. Combined output for the regression
example: `['src', 'src/**/*.ts']` — covers both the directory-
level and file-level skill cases.

Adds three tests: an updated unit test pinning the joined
effective selector, an absolute-`path` variant whose joined form
gets rejected downstream by the project-root guard
(`/tmp/external/**/*.ts`), and the audit-suggested integration
regression that pipes `extractToolFilePaths` output straight into
`SkillActivationRegistry` and verifies a `paths: ['src/**/*.ts']`
skill activates from `glob({ path: 'src', pattern: '**/*.ts' })`.

* fix(skills): join glob.path with glob.pattern as effective selector

Two coupled fixes for the glob-pattern extraction landed in 7cb7145bb:

1. **Windows CI failure.** `path.join('src', '**/*.ts')` returns
   `'src\\**\\*.ts'` on Windows (OS-aware separator). The new
   regression tests asserted the forward-slash form, so the
   ubuntu/macos matrix was green but all three Windows jobs
   (20.x/22.x/24.x) failed. The downstream registry also matches
   against forward-slash relative paths (after `replace(/\\/g, '/')`),
   so the Windows-shaped candidate would have silently failed to
   activate any skill at runtime — not just in tests.

2. **`..` normalization.** `path.join('src', '../*.ts')` collapses to
   `'*.ts'`, losing the information that the glob actually escaped
   its `path` root. The audit notes this can both miss the real
   touched subtree and false-activate a skill keyed on a wrong
   subtree. Concat preserves the selector verbatim.

Replace `path.join(pathField, patternField)` with
`${pathField.replace(/[\\/]+$/, '')}/${patternField}` per the
audit's exact suggestion. Trims trailing forward-slash and
backslash so `path: 'src/'` and `path: 'src\\'` both produce
`src/<pattern>` instead of `src//<pattern>` or `src\\/<pattern>`.

Adds three tests covering: `..` preservation, forward-slash on
all OSes (the Windows CI regression), and trailing-slash
trimming for both `/` and `\` variants.

* fix(skills): silence CodeQL ReDoS flag on trailing-separator trim

CodeQL #145 flagged `pathField.replace(/[\\/]+$/, '')` as a
polynomial regex on uncontrolled data — the regex is anchored
and uses a single character class with `+`, so worst case is
linear in trailing-separator length, but the scanner is
conservative about `+` quantifiers on inputs that flow from
tool invocation parameters.

Replace the regex with an explicit `endsWith` loop. Same O(n)
behavior on the trailing run, no regex for CodeQL to chew on.
Existing trailing-slash test (forward and back) still passes.

* fix(skills): comprehensive review pass — security, correctness, robustness

Eleven findings from /qreview (claude-opus-4-7), grouped by area:

CORRECTNESS

- C1: appendAdditionalContext silently dropped reminders for any tool
  whose llmContent is a single non-array Part (read-file returning
  inlineData for images / PDFs is the canonical case). Both the
  ConditionalRulesRegistry rule reminder and the path-conditional
  skill activation reminder were lost. Wrap the single-Part case
  into an array so the addition still lands.
- S2: Legacy tool-name aliases (`replace` → `edit`,
  `search_file_content` → `grep_search`, `task` → `agent`) bypassed
  FS_PATH_TOOL_NAMES. The registry resolves the alias at execute time
  but `request.name` keeps the alias, so `replace({ file_path: ... })`
  produced empty candidates and missed activation. Canonicalize via
  `ToolNamesMigration` before the allowlist check.
- S5: `new SkillActivationRegistry(...)` ran picomatch unguarded —
  pathological patterns (oversize / broken extglob) could throw and
  abort all of `refreshCache`. Wrap each picomatch call in try/catch
  inside the constructor; drop the bad pattern, keep the rest of
  the skill, log via debugLogger.
- S7: Extension parser (skill-load.ts) silently dropped
  `disable-model-invocation` and `when_to_use`. Now that we have
  `paths:`, that meant an extension SKILL.md with both `paths:` and
  `disable-model-invocation: true` would still fire path-activation
  reminders for a skill the model can't invoke — directly
  contradicting the bug_004 fix at the project/user level.
- S8: SkillTool discarded the `addChangeListener` cleanup function
  and had no `dispose()`. Subagents share the parent's SkillManager
  via `InProcessBackend.createPerAgentConfig`, so each per-subagent
  SkillTool registered another listener; with the listener pipeline
  now async, every path activation serialized through every stale
  subagent's refresh chain. Mirror AgentTool: store the cleanup,
  expose `dispose()`.

SECURITY / SUPPLY-CHAIN

- S11: `validateSkillName`'s `/^[a-zA-Z0-9_:.-]+$/` rejected every
  non-ASCII name on upgrade, silently dropping CJK / Cyrillic /
  accented Latin skills. The structural-injection guard targets
  `<>"'/\n\r\t` etc; entire Unicode planes are not the threat.
  Widen to `/^[\p{L}\p{N}_:.-]+$/u`. Update docs/users/features/
  skills.md to match.
- S10: `parsePathsField` only validated shape (must-be-array). Now
  also reject leading-slash absolute patterns and `..` parent-escape
  patterns at parse time — these silently never match anything in
  the activation registry, so an author who writes `paths:
  ['/etc/passwd']` or `['../*.ts']` would otherwise see the skill in
  /skills and never understand why it never activates.

ROBUSTNESS

- S3: `coreToolScheduler` emitted "skill X is now available via the
  Skill tool" even when the calling subagent's tool registry did not
  expose SkillTool (subagent's `tools:` allowlist excluded `skill`).
  Gate the reminder on `toolRegistry.getTool(ToolNames.SKILL)`.
- S4: `extensionManager.refreshMemory` used `Promise.all` so a
  rejection from skill or subagent refresh nuked the other leg AND
  the hierarchical-memory refresh below it. Switch to
  `Promise.allSettled`, log each rejection, and `await` the
  hierarchical refresh too (the comment justifies awaiting; the
  code didn't).
- S9 / S12: `docs/users/features/skills.md` claimed `paths:` only
  gates model discovery and slash invocation always works. True for
  the user-side path itself, but if the model then tries to chain
  off the user's invocation (call `Skill { skill: ... }` itself),
  validateToolParams returns "gated by path-based activation" —
  contradicting the doc. Rephrase to call out the model-side
  limitation explicitly.

DEFERRED

- S6: notifyChangeListeners swallows per-listener errors and the
  reminder still fires. Real concern but the fix needs an API
  shape change (listener-failure signal back to the scheduler);
  worth its own design discussion. Logged here for follow-up.

Adds 12 regression tests across the 7 affected files. 632 tests
pass; types and lint clean.

* fix(skills): activate broad globs on dotfiles + cross-ref FS allowlist

Two more findings from /review:

- S13: picomatch was compiled with `dot: false`, so a broad glob like
  `paths: ['**/*.js']` silently excluded `.eslintrc.js`, `.env`,
  `.github/foo.yml`, etc. The hidden-file exclusion is gitignore-style
  semantics — wrong for activation, where the question is "did the
  model touch a file matching this glob." Switch to `dot: true`.

- S14: `FS_PATH_TOOL_NAMES` is a manually maintained allowlist with no
  compile-time guard — adding a new FS tool without updating the set
  silently drops the tool out of the activation pipeline. Add a
  cross-ref comment at the top of `ToolNames` in `tool-names.ts`
  pointing maintainers at the allowlist site, plus a TODO noting the
  long-term fix is per-declaration `pathFields?: string[]`. The
  cross-cutting refactor is its own PR.

Adds one regression test (`activates broad globs on dotfiles too`)
that pins the dot:true semantics on `**/*.js` matching
`.eslintrc.js`. 211 skill-area tests pass.

* fix(skills): per-tool extraction dispatcher (LSP URI + grep glob + integration test)

Four findings from /review on the activation extractor:

C1 (Critical): LSP allowlisted but the extractor pushed `filePath`
  through unchanged. The LSP tool accepts non-file URI schemes
  (`http://`, `git://`, etc.); forwarding any of those to
  SkillActivationRegistry as a project-relative candidate let an
  LSP call against a non-file resource activate path-gated skills
  without the model touching a real project file. Fix is two-part:
  decode `file://` URIs via `fileURLToPath` (so a project file
  expressed as a URI still activates correctly) and silently drop
  any string containing `://` that's not `file://`.

S1: LSP `incomingCalls` / `outgoingCalls` operate on
  `callHierarchyItem.uri`, not the top-level `filePath`. After
  `prepareCallHierarchy` returns a file-backed item, following the
  hierarchy with that item produced no candidate, so path-gated
  skills for that file stayed dormant. Same URI-aware extraction is
  applied to the nested `uri` field.

S2: grep_search has a path-shaped `glob` field
  (`GrepToolParams.glob`) — distinct from `pattern`, which is a
  regex on contents. The extractor previously ignored `glob`, so
  `grep_search({ pattern, glob: 'src/**/*.ts' })` produced no
  activation candidate even though the call walked every file under
  `src/**/*.ts`. Same `path + glob` join treatment as GLOB.

S3: No scheduler-side integration test covered the
  extractToolFilePaths → matchAndActivateByPaths → reminder-append
  wiring, so a regression there could land while extractor and
  registry unit tests still passed. Added three integration tests
  covering: (a) reminder appended when SkillTool present,
  (b) reminder suppressed when SkillTool absent (subagent case),
  (c) hook not invoked for non-FS tools.

Restructured `extractToolFilePaths` from a generic
`file_path/filePath/path/paths` extractor into a per-tool
dispatcher (`switch` on canonical tool name). The previous generic
shape was overly permissive — every FS tool got every field name,
including ones it doesn't accept — and it was the wrong shape to
add LSP URI semantics to. Per-tool means each branch reflects the
actual `XToolParams` interface.

Test reshape:
- Removed tests asserting cross-tool field acceptance (e.g. grep
  reading `filePath` / `paths`); those documented inaccurate input.
- Added per-tool realistic tests for grep glob, lsp file:// URI,
  lsp callHierarchyItem.uri, lsp non-file scheme dropped.
- Plus the three CoreToolScheduler activation wiring tests.

639 tests pass (was 632); types and lint clean.

DEFERRED

S4: Activation driven from input selector rather than concrete
  matched files. For `glob({ pattern: '**/*.ts' })` the selector
  itself may not match a skill scoped narrower than the query.
  Real concern, but the fix needs typed result-path metadata
  feedback from each tool — a cross-cutting addition to every FS
  tool's return shape. Logged for follow-up.

* fix(skills): make LSP URI tests platform-portable for Windows CI

Two of the new LSP tests in 58836f1c3 hard-coded `file:///proj/...`
URIs. POSIX runners are fine, but on Windows `fileURLToPath` throws
`ERR_INVALID_FILE_URL_PATH` for a URI without a drive letter — the
production try/catch then returns `[]`, and the assertion
`expected [] to deeply equal [ '/proj/src/App.ts' ]` fails.

Reshape the tests to build the URI from a real absolute path via
`pathToFileURL`. The URI shape becomes the host's natural form
(`file:///tmp/...` on POSIX, `file:///C:/.../tmp/...` on Windows),
and the round-trip through `fileURLToPath` always succeeds.

Production code unchanged.

* fix(skills): XML-escape description/whenToUse; symlinks in skill-load.ts; dot:true in ConditionalRulesRegistry

Agent-Logs-Url: https://github.com/QwenLM/qwen-code/sessions/a56d83ce-cbdf-4213-a90a-888a9f05ee4f

* fix(skills): backport Windows cross-drive guard to ConditionalRulesRegistry

The latest /review (deepseek-v4-pro) flagged divergence between
SkillActivationRegistry (which has the
`pathModule.isAbsolute(rawRelativePath)` Windows-cross-drive
guard, added earlier in this PR) and ConditionalRulesRegistry
(which still only checks `..` / `../` prefixes). On Windows,
`path.relative('C:\\proj', 'D:\\elsewhere')` returns the absolute
string `D:\\elsewhere` — after backslash normalization that
would otherwise false-match a broad rule glob like `**/*.ts`.

Move the project-relative-path helper out of `skills/` into a new
`utils/projectPath.ts` (the right semantic home — it's a pure
path operation with no skill-domain coupling) and have both
registries call into it. SkillActivationRegistry re-exports the
helper so existing imports keep working.

Adds a regression test in `rulesDiscovery.test.ts` for the
off-project path case (covers both POSIX `..` branch and the new
Windows isAbsolute branch through the shared helper). Direct
`path.win32`-parameterized cover already lives in
`skill-activation.test.ts`. 252 skill+rules tests pass.

* test(skills): pin XML escaping on modelInvocableCommands description too

cmd.description already routes through escapeXml in skill.ts:204
(landed in b1d9324f5), but no test pinned the cmd path — only the
skill.description / whenToUse path. Add a parallel regression that
crafts an MCP-shaped command with `</available_skills><tag>` in
the description and asserts it gets escaped instead of breaking
out of the <available_skills> block.

* fix(skills): escape cmd.name; extension skillRoot; surface invalid globs

Three findings from /review (deepseek-v4-pro):

C1: `cmd.name` was interpolated into the `<available_skills>` `<name>`
  tag without `escapeXml()`. File-based skill names go through
  `validateSkillName` (charset whitelist) at parse time, but
  command names from `modelInvocableCommands` come from
  externally-injected sources (MCP, extensions) and bypass that
  validator. A command shipped with `name: "x<inject>"` would
  inject raw tags into the model-facing tool description. Wrap
  `cmd.name` in `escapeXml`, parallel to the existing
  `cmd.description` escape one line below.

C2: `parseSkillContent` in `skill-load.ts` (the extension parser)
  never set `skillRoot: path.dirname(filePath)`. The
  project/user/bundled parser in `skill-manager.ts` does, and
  `registerSkillHooks.ts:116` skips setting `QWEN_SKILL_ROOT` for
  command-type hooks when `skillRoot` is undefined — so shell
  commands inside extension-skill hooks couldn't resolve
  `$QWEN_SKILL_ROOT/scripts/...` references. Add the field.
  Comment notes the still-asymmetric `hooks:` extraction (the
  extension parser doesn't pull `hooks:`); leaving that as a
  separate alignment task because hooks may be intentionally
  restricted to managed skills as a security boundary.

S3: Invalid `paths:` globs were only logged at debug level.
  Author writes `src/***/file.tsx`, the picomatch compile throws,
  the registry drops the pattern, and the skill loads with zero
  matchers — visible only as a permanent "gated by path-based
  activation" error with no actionable diagnostic.

  Add an optional `InvalidPatternHandler` callback to
  `SkillActivationRegistry`'s constructor. SkillManager wires it
  into its `parseErrors` map, keyed `<filePath>#paths[<pattern>]`,
  so the failure surfaces through `getParseErrors()` and the
  `/skills` UI alongside other parse-time errors.

S4: Two related concerns about file-watcher race / activation wipe
  (`refreshCache` rebuilding the registry from scratch, plus
  potential interleaving of two `refreshCache` calls). Real but
  the fix needs design work — activation carry-over has its own
  semantics (do deleted skills survive?), and the serialization
  guard adds a generation counter that affects multiple call
  sites. Logged for follow-up.

Three regression tests added: cmd.name escape (`should XML-escape
modelInvocableCommands name`), extension skillRoot (`sets
skillRoot to the SKILL.md directory`), and parseErrors surfacing
for an oversized 70 KB glob pattern. 205 skill-related tests pass.

* fix(skills): comprehensive XML-escape + coalesce + parallel listeners

Six findings from /review (deepseek-v4-pro):

C1: skill.name interpolated raw into <available_skills>. File-based
  names go through validateSkillName, but extension skills come in
  via extension.skills (skill-manager.ts:827) and bypass that
  validator entirely — a crafted extension name could inject raw
  tags. Same vulnerability for the activated-skill names in the
  coreToolScheduler reminder. Wrap both in escapeXml.

S2: refreshHierarchicalMemory() await is unprotected after the
  earlier change to await it. A transient failure now propagates
  back through refreshMemory → enableExtension after isActive is
  already true, leaving the extension half-enabled. Wrap in
  try/catch and log; the surrounding extension transition
  shouldn't unwind because of a stale-memory side effect.

S3: escapeXml duplicated between skill.ts and background-tasks.ts.
  Extract to utils/xml.ts; both call sites import from there.

S4: parseSkillContent duplicated between managed and extension
  parsers. Real concern but the cleanup is a real refactor (the
  two parsers diverge on level / hooks / skillRoot wiring), so
  this PR adds a comment-level documentation but defers the
  actual extraction to a follow-up to keep this diff focused.

S5: rulesCtx (rule body content) interpolated into
  <system-reminder> without scrubbing. A rule whose content
  contained literal `</system-reminder>` (e.g. a doc rule about
  reminders) would close the envelope early. Apply a targeted
  scrub of the closing-tag literal in the joined body. Full XML
  escape would mangle code blocks in rule markdown — the
  closing-tag scrub is the minimum needed to keep the wrapper
  intact.

S6: notifyChangeListeners awaited listeners sequentially. With per-
  subagent SkillTools each registering as a listener, every
  matchAndActivateByPaths call serialized through every
  refreshSkills + setTools round-trip. Switch to
  Promise.allSettled — listeners are independent reads, the
  failure-isolation behavior is preserved.

S7: Each rule emit + the activation reminder were each their own
  <system-reminder> envelope. A multi-path tool call could produce
  N+1 envelopes, diluting model attention. Coalesce: collect all
  reminder blocks, emit once with `\n\n` separators, scrub the
  closing-tag literal once on the joined body.

Tests added:
- skill.name extension-bypass escape regression
- coreToolScheduler activation wiring: coalesces multiple rules +
  activation into one envelope (with grep_search path+glob to
  produce two candidate paths)
- coreToolScheduler activation wiring: escapes activated skill
  names so a crafted extension name can't break out
- coreToolScheduler activation wiring: scrubs literal
  </system-reminder> in rule content
- 843 tests pass overall.

* fix(skills): symlink scope check + dispose on stopAgent + listener type

Five findings from /review (Qwen3.6-Plus-DogFooding):

C1 + C2 (Critical, same finding cited twice): Symlink target was
  validated for "is a directory" but not for "stays inside
  baseDir". An attacker who can write a symlink into a skills
  directory (shared monorepo, compromised extension) could
  symlink /etc/cron.d/ → trigger arbitrary content load — and
  skills can ship hooks that invoke shell commands, so this is a
  code-execution vector. Apply realpath + prefix check in both
  symlink branches (skill-load.ts AND skill-manager.ts).
  Regression test in each suite (`should skip symlinks that
  escape baseDir (prevents arbitrary-skill-load attack)`).

C3: SkillTool.dispose() existed but was only called from
  ToolRegistry.stop() at full shutdown. Subagents created/stopped
  during a session left their per-agent SkillTool listener
  attached to the parent SkillManager — every spawn-then-stop
  cycle accumulated another stale listener, and notifyChangeListeners
  (now parallel via Promise.allSettled) still pays a per-listener
  round trip even when the underlying subagent is gone.

  Convert InProcessBackend.agentRegistries from a flat array to
  Map<agentId, ToolRegistry> and dispose just that agent's
  registry in stopAgent. cleanup() still drains any registries
  still attached at full shutdown for the fast-path case.

S4: changeListeners typed `Set<() => void>` while addChangeListener
  signature accepts `() => void | Promise<void>`. The runtime
  Promise.resolve().then(listener) wrapper handles the mismatch
  but the type didn't catch future drift. Widen the field type
  to match the parameter signature.

S6: FS_PATH_TOOL_NAMES allowlist has no compile-time guard.
  Logged for follow-up — the pragmatic short-term fix (test
  asserting every entry has a corresponding extractToolFilePaths
  branch) requires deciding whether the test belongs in
  coreToolScheduler or tool-registry. Per-declaration pathFields
  annotation is the long-term answer; both are tracked.

S7: setTools concurrency. Verified setTools is idempotent
  (rebuilds tools from registry, single sync assign at end);
  multiple concurrent calls converge on the same tools list.
  Added an inline note rather than a runtime mutex.

Defer:
- S5: refreshCache wipes all activations. Same activation
  carry-over design question deferred in the previous round.

* fix(skills): listener timeout, full XML escape, allowlist warning + tests

Address inline review feedback:

- skill-manager.notifyChangeListeners: 30s per-listener timeout via
  Promise.race so a hung listener (e.g. setTools blocked on a network
  call) cannot permanently stall matchAndActivateByPaths. Timer is
  unref'd to avoid keeping the event loop alive.

- types.parsePathsField: tighten parse-time validation. Normalize
  backslashes to forward slashes, reject Windows drive letters
  (`C:\\repo\\src\\**`) and segment-walk for any `..` (catches
  `./../*.ts`, `src/../../**`, `..\\secret\\*.ts`). Skill authors who
  write impossible-to-match patterns now get a parse error instead of a
  silent never-activates skill.

- utils/xml.escapeXml: widen to all five XML metacharacters
  (`&<>"'`), not just three. Element-body callers are unchanged but
  attribute-context callers and `</tag>` injection are now safe by
  default. monitorRegistry drops its local copy in favor of the shared
  helper.

- coreToolScheduler.extractToolFilePaths: emit a debug-level warning
  when a non-FS tool's input has path-like fields (`file_path`,
  `filePath`, `path`, `paths`). Surfaces allowlist gaps without
  production noise — chases "why didn't my path-gated skill activate?".

- Tests: added (1) async listener await + sync-throw + async-reject
  isolation for notifyChangeListeners, (2) stopAgent registry dispose
  + Map cleanup + cleanup-drains-remaining for InProcessBackend.

* fix(skills): harden symlink containment checks

* Revert "fix(skills): harden symlink containment checks"

This reverts commit 7e70a25a3a.

* fix(skills): clear listener timeout, share symlink scope helper

- skill-manager.notifyChangeListeners: clear the per-listener
  setTimeout in `.finally(...)` once the race settles. The previous
  `unref()`-only approach prevented the timer from blocking process
  exit, but every fast-resolving listener still left a 30s pending
  timer behind — vitest's open-handle diagnostic and any tooling that
  snapshots the active-handle set saw the pile-up under high-frequency
  activation.

- New skills/symlinkScope.ts: shared `validateSymlinkScope` helper.
  Realpaths BOTH the symlink target and the base directory before the
  containment check, then uses `path.relative` (rather than
  `realPath.startsWith(base + sep)`) for cross-platform safety. The
  prior asymmetric form — `realpath(target)` against the raw
  `path.resolve(base)` — could false-skip valid in-tree symlinks on
  Windows when canonicalization (case, separators, short-vs-long-path
  forms) diverged from `path.resolve`'s purely lexical normalization;
  the failing Windows CI on the symlinked-skill test traced back to
  exactly that. `path.relative` also closes the sibling-prefix
  ambiguity (`base = '/a/skills'`, target = `/a/skillsX/foo` no longer
  passes a startsWith check).

- skill-load.ts and skill-manager.ts both delegate to the shared
  helper. Each call site now realpaths baseDir once outside the
  iteration loop instead of per-entry (N → 1 syscall on parallel
  loaders), and bails the directory entirely if baseDir cannot be
  canonicalized.

- Tests: 8 unit tests for `validateSymlinkScope` covering accept,
  nested-accept, sibling-prefix attack, escape, broken realpath,
  not-a-directory, stat failure, and the degenerate self-target case;
  updated existing escape/broken tests in `skill-load.test.ts` /
  `skill-manager.test.ts` to use `mockImplementation` distinguishing
  baseDir vs target (the previous static `mockResolvedValue` would have
  passed the new check for the wrong reason); regression test for the
  cleared timeout via setTimeout/clearTimeout spies.

* fix(skills): segment-aware symlink containment, accepts ..-prefixed names

The previous `rel.startsWith('..')` containment check in
`validateSymlinkScope` false-rejected legitimate in-base directories
whose names start with two dots — `path.relative('/base', '/base/..shared/foo')`
returns `'..shared/foo'`, which is a real filename shape, not a
parent-traversal escape.

Switch to a segment walk: `rel.split(/[/\\]/)[0] === '..'` correctly
distinguishes:
  - `'../foo'`         → segments[0] = '..'      → escapes ✓
  - `'..shared/foo'`   → segments[0] = '..shared' → in-scope ✓
  - `'..bar'`          → segments[0] = '..bar'    → in-scope ✓
  - `'..\\foo'` (Win)  → segments[0] = '..'      → escapes ✓

Tests: two new regressions in `symlinkScope.test.ts` covering the
multi-segment (`..shared/foo`) and single-segment (`..bar`) cases.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yiliang114 <1204183885@qq.com>
2026-05-05 00:28:53 +08:00
Shaojin Wen
35fe97e0f6
feat(review): expand review pipeline + qwen review CLI subcommands (#3754)
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(review): expand review pipeline + add `qwen review` CLI subcommands

Review skill (SKILL.md) changes:
- Step 4: 5 → 9 parallel agents (split Correctness/Security, add Test
  Coverage, 3 undirected personas: attacker / 3am-oncall / maintainer)
- Step 5: verification "uncertain → reject" → "uncertain → low-confidence"
  (terminal-only "Needs Human Review" bucket; never posted as PR comments)
- Step 6: single reverse audit → iterative (terminate on no-new-findings,
  hard cap 3 rounds)
- Step 9: self-PR detection (downgrade APPROVE/REQUEST_CHANGES → COMMENT
  when GitHub forbids self-review with HTTP 422); CI status check
  (downgrade APPROVE → COMMENT on red/pending CI); existing-Qwen-comment
  classification with priority order Stale > Resolved > Overlap > NoConflict
  (only Overlap blocks for confirmation)

`qwen review` CLI subcommands (packages/cli/src/commands/review/):
- fetch-pr     — clean stale + fetch PR ref + create worktree + metadata
- pr-context   — emit Markdown context file with security preamble +
                 already-discussed dedup section
- load-rules   — read review rules from base branch (4 source files)
- deterministic— run tsc, eslint, ruff, cargo-clippy, go-vet, golangci-lint
                 on changed files; filtered + structured findings JSON
                 (TypeScript/JavaScript, Python, Rust, Go)
- presubmit    — self-PR + CI status + existing-comment classification in
                 a single JSON report
- cleanup      — worktree + branch ref + per-target temp files (idempotent)

Cross-platform: execFileSync (no shell), path.join, CRLF normalization,
which/where for tool detection. Replaces bash-style inline commands in
SKILL.md; works identically on macOS/Linux/Windows.

Path consistency: SKILL.md temp files moved from /tmp/qwen-review-* to
.qwen/tmp/qwen-review-* — matches what os.tmpdir() resolves to across
platforms (macOS returns /var/folders/... not /tmp).

DESIGN.md gains five "Why ..." sections explaining each design decision;
docs/users/features/code-review.md synced for user-visible changes.

* feat(review): expose full reply chains in pr-context output

`qwen review pr-context` now renders each replied-to inline-comment thread
as the original reviewer comment + chronological reply chain, instead of
only listing the root-comment snippet. This lets review agents see at a
glance whether a topic has been addressed (e.g. a "Fixed in <commit>"
reply closes the thread) and avoids re-reporting already-resolved
concerns without forcing the LLM driver to manually summarise each reply
chain in agent prompts.

- Walk `in_reply_to_id` chain to group replies under their root comment
- Sort replies chronologically (by id, monotonic on GitHub)
- Render thread block: root snippet as a quote + bulleted reply list
- Sort threads by `(path, line)` for deterministic output
- SKILL.md note updated to point agents at the new chain format

* feat(review): include review-level summaries in pr-context output

`qwen review pr-context` now also fetches `gh api repos/{owner}/{repo}/pulls/{n}/reviews`
and renders a "Review summaries" section listing each reviewer's
overall body (the comment they typed alongside an APPROVED /
CHANGES_REQUESTED / COMMENTED submission). Closes a real gap found
during the PR #3684 review:

> "@wenshao [CHANGES_REQUESTED]: The previously identified exported
> type rename issue no longer maps to the current PR diff, so this
> review only includes the remaining high-confidence blocker."

Without this section, the LLM driver's review agents would have missed
that integration note from the prior reviewer.

- New `RawReview` type + extra `ghApi` call
- Filter: skip empty bodies + the canonical "No issues found. LGTM!"
  template the qwen-review pipeline auto-emits — those carry no
  agent-actionable content beyond the review state itself
- Sort meaningful reviews by `submitted_at` for chronological output
- Stdout summary now reports `M/N review summaries` (M = kept after
  filter)

Smoke-tested on PR #3684: 30 inline, 3 issue, 1/30 review summaries
correctly surfaces the @wenshao CHANGES_REQUESTED body and filters the
29 LGTM templates.

* fix(review): paginate gh API calls to capture comments past page 1

`gh api <path>` defaults to per_page=30. Busy PRs cross that limit on
inline comments, issue comments, and reviews — the latest entries (the
ones most likely to contain new reviewer feedback or in-flight reply
chains) end up on page 2+ and were silently truncated.

Concrete bug found while re-reviewing PR #3684:
  Before: `30 inline, 3 issue comments, 1/30 review summaries`
  After:  `97 inline, 3 issue comments, 6/67 review summaries`

5 additional reviewer-level summaries surfaced — including the
@wenshao 2026-04-30 "Multi-agent re-review (Phase C)" body with the
explicit verification notes that this PR's pipeline is supposed to
chain forward into the next review.

Changes:
- `lib/gh.ts`: new `ghApiAll(path)` helper using `gh api --paginate`,
  which walks every `next` link and concatenates each page's array.
- `pr-context.ts`: 3 fetches (inline / issue / reviews) → `ghApiAll`.
- `presubmit.ts`: PR comments fetch → `ghApiAll` too (existing-comment
  classification was equally susceptible to dropping page 2+ overlap
  candidates).

`check-runs` and `commits/<sha>/status` calls retain `ghApi` — those
return objects (with embedded arrays) and rarely cross 30 entries.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-05-01 18:30:35 +08:00
易良
49e462c021
fix(lsp): 修复 LSP 文档、isPathSafe 限制,并提升 LSP 工具调用率 (#3615)
* fix(docs): correct outdated and inaccurate LSP documentation

- Remove reference to non-existent `packages/cli/LSP_DEBUGGING_GUIDE.md`
- Remove reference to unimplemented `/lsp status` slash command
- Replace incorrect `DEBUG=lsp*` env var with actual debug log location
  (`~/.qwen/debug/` session files with `[LSP]` tag)
- Remove external Claude Code documentation links (`code.claude.com`)
- Document `isPathSafe` constraint: absolute paths outside workspace
  are blocked, users must add server binary directory to PATH
- Add practical troubleshooting: `ps aux | grep <server>` to check
  if the server process is actually running
- Add clangd-specific guidance: `--background-index`, `compile_commands.json`
  location, and `--compile-commands-dir` usage
- Simplify trust documentation (remove vague "configure in settings")

* fix(lsp): allow absolute paths in LSP server command configuration

Previously, `isPathSafe` rejected any command containing a path
separator that resolved outside the workspace directory. This blocked
legitimate use cases where users specify absolute paths to language
server binaries (e.g. `/usr/bin/clangd`, `/opt/tools/jdtls/bin/jdtls`).

The fix allows:
- Bare command names resolved via PATH (unchanged)
- Absolute paths (explicit user intent, already gated by trust checks)
- Relative paths within the workspace (unchanged)

Only relative paths that traverse outside the workspace (e.g.
`../../malicious-binary`) are still blocked.

Closes: server silently fails to start when users configure absolute
paths in `.lsp.json`, with only a debug log warning visible.

* feat(lsp): inject LSP priority instruction into system prompt when enabled

The model was not using the LSP tool because the system prompt's
"Tool Usage" section never mentioned it. The tool description alone
("ALWAYS use LSP as the PRIMARY tool") was insufficient — models
follow system prompt instructions more reliably than tool descriptions.

Changes:
- getCoreSystemPrompt() accepts `options.lspEnabled` parameter
- When LSP is enabled, injects an instruction in the Tool Usage section
  telling the model to ALWAYS use the LSP tool FIRST for code
  intelligence queries (definitions, references, hover, symbols, etc.)
  instead of falling back to grep/readfile
- Updated client.ts to pass config.isLspEnabled() to the prompt builder
- Updated test mocks and snapshots

* feat(lsp): add symbolName parameter for position-free LSP queries

The model avoided calling LSP for findReferences, hover, etc. because
these operations required filePath + line + character which the user
rarely provides. The model would read files directly instead.

Changes:
- Add `symbolName` optional parameter to LspTool
- When symbolName is provided without line/character, auto-resolve
  the symbol's position via workspaceSymbol before executing the
  actual operation (findReferences, hover, goToImplementation, etc.)
- Update tool description with examples showing symbolName usage
- Move LSP priority instruction to top of system prompt for visibility
- Add debug logging for LSP prompt injection

This enables natural queries like:
  {operation: "findReferences", symbolName: "Calculator"}
  {operation: "hover", symbolName: "addShape"}
without requiring the user to know exact file positions.

* feat(lsp): add LSP reminder to grep/readfile tool descriptions

When LSP is enabled, the model often chose grep or readfile instead
of LSP for code intelligence queries. Now the competing tools'
descriptions include a note reminding the model to use the LSP tool
for definitions, references, symbols, hover, diagnostics, etc.

This "push-pull" approach:
- System prompt pushes toward LSP (top-level priority instruction)
- Grep/ReadFile descriptions pull away from code intelligence usage

* fix(docs): align LSP doc with isPathSafe change — absolute paths now supported

The doc still said "absolute paths outside the workspace are not
supported" but the code was changed to allow them. Updated all
three places (Required Fields table, Troubleshooting, Debugging)
to reflect that absolute paths are now accepted.

* fix(lsp): improve symbol-based tool resolution

* fix(lsp): normalize display paths across platforms

* fix(lsp): narrow docs and path safety changes

* fix(lsp): add edge-case tests for isPathSafe and fix Chinese comment

- Add test for intermediate path traversal (./a/../../../etc/passwd)
- Add test for forward-slash relative paths (tools/clangd)
- Replace Chinese JSDoc with English on requestUserConsent

* fix(lsp): rename requestUserConsent to checkWorkspaceTrust

The method only checks workspace trust level and does not actually
prompt the user for consent. Rename the method and update the JSDoc
and call-site log message to accurately reflect the behavior.
2026-04-30 15:24:18 +08:00
jinye
f0e8601982
fix(cli): add API Key option to qwen auth interactive menu (#3624)
* fix(cli): add "API Key" option to `qwen auth` interactive menu

The `qwen auth` CLI command only showed 2 options (Coding Plan, Qwen OAuth),
while the interactive `/auth` dialog showed 3 (Coding Plan, API Key, Qwen OAuth).
Users following the README instructions to configure OpenRouter/Fireworks via
`qwen auth` had no API Key entry point.

- Add "API Key" option to the `runInteractiveAuth` menu with two sub-paths:
  "Alibaba Cloud ModelStudio Standard API Key" (guided flow) and
  "Custom API Key" (prints docs link)
- Add `qwen auth api-key` yargs subcommand for direct access
- Extract `createMinimalArgv` / `loadAuthConfig` helpers to eliminate duplicated
  CliArgs boilerplate
- Extract `promptForInput` to share raw-mode stdin logic between `promptForKey`
  and `promptForModelIds`
- Improve `showAuthStatus` to distinguish Coding Plan, Standard API Key, and
  generic OpenAI-compatible configurations
- Align menu labels and descriptions with the interactive `/auth` dialog

Closes #3413

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

* docs: add `qwen auth api-key` to auth subcommand tables

Update documentation to reflect the new `qwen auth api-key` subcommand:
- auth.md: add to subcommands table, examples, and interactive menu display
- commands.md: add to CLI Auth Subcommands table
- quickstart.md: add to quick-reference command table

* fix(cli): restore incomplete Coding Plan warning in showAuthStatus

When selectedType is USE_OPENAI and Coding Plan metadata exists but
the API key is missing, show the incomplete warning instead of falling
through to the generic "OpenAI-compatible" status.

* refactor(cli): use endpoint constants in region selector and fix status formatting

- Use ALIBABA_STANDARD_API_KEY_ENDPOINTS constants for region
  descriptions instead of hardcoded URLs
- Restore trailing newline in showAuthStatus "no auth" command list
  for consistent spacing

* fix(cli): determine active auth method from model config in showAuthStatus

Previously showAuthStatus checked which env keys exist to determine
the auth method, causing false reports when users switch providers
(e.g., Coding Plan key still present after switching to Standard API Key).

Now it inspects the active model's provider config (baseUrl/envKey) to
determine the actual method, and validates the corresponding key exists:
- Coding Plan: check via isCodingPlanConfig + CODING_PLAN_ENV_KEY
- Standard API Key: check via DASHSCOPE_STANDARD_API_KEY_ENV_KEY + endpoints
- Generic OpenAI-compatible: check if the model's envKey is set

Also clear stale Coding Plan metadata (codingPlan.region/version and
process.env) when switching to Standard API Key.

* fix(cli): add legacy fallback in showAuthStatus and clear persisted Coding Plan env

- When no active model config is found (legacy setups without
  modelProviders), fall back to env key / metadata checks for
  Coding Plan status detection. Fixes CI test failures.
- When activeConfig exists but has no envKey, report incomplete
  status instead of false positive "Configured".
- Clear persisted env.BAILIAN_CODING_PLAN_API_KEY from settings
  when switching to Standard API Key, not just process.env.

* fix(cli): also remove Coding Plan model entries when switching to Standard API Key

When switching to Standard API Key, filter out existing Coding Plan
model entries from modelProviders.openai in addition to old Standard
entries. Previously these were preserved but their credential source
(BAILIAN_CODING_PLAN_API_KEY) was cleared, leaving broken model
entries visible in /model.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-27 22:01:47 +08:00
Shaojin Wen
f420742831
feat(cli,core): LLM-generated summary labels for tool-call batches (#3538)
Some checks are pending
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
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
* feat(cli,core): generate tool-use summaries for compact mode

After each tool batch completes, fire a parallel fast-model call to
generate a short git-commit-subject-style label summarizing what the
batch accomplished (e.g. "Read txt files", "Searched in auth/"). In
compact mode the label replaces the generic "Tool × N" header so N
parallel tool calls collapse to a single semantic row.

The fast-model call (~1s) runs fire-and-forget, overlapped with the
next turn's API stream, so there is no perceived latency. Missing
fast model, aborted turns, and model failures all degrade silently to
the existing rendering.

The summary is also emitted as a `tool_use_summary` history entry
with `precedingToolUseIds`, keeping the shape compatible with SDK
clients that want to render collapsed tool views on their own.

Gated by `experimental.emitToolUseSummaries` (default on). Can be
overridden per-session with `QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1`.

The system prompt and truncation rules (300 chars per tool field,
200 chars of trailing assistant text as intent prefix) match the
existing behavior seen in other tools that emit the same message
type, so SDK consumers see a consistent shape across clients.

* fix(core): bound cleanSummary quote-strip regex to avoid ReDoS

CodeQL js/polynomial-redos flagged the /^["'`]+|["'`]+$/g pattern in
cleanSummary because its input comes from an LLM (treated as
uncontrolled). The original regex is anchored and linear in practice,
but tightening the quantifier to {1,10} both satisfies the static
check and caps engine work on pathological model output with a long
run of quotes. Ten opening/closing quotes is well past anything a real
label would produce.

* fix(cli): render tool_use_summary inline so full mode also shows the label

The summary was only visible in compact mode because the full-mode
ToolGroupMessage ignored the compactLabel prop. Compact mode got away
with this because mergeCompactToolGroups triggers refreshStatic(),
which re-renders the merged tool_group with its newly-looked-up
label. Full mode has no such refresh path, so when the fast-model
call resolves *after* the tool_group has been committed to the
append-only <Static>, there is no way to retroactively decorate it.

Switch to rendering `tool_use_summary` as its own inline history item
(a single dim `● <label>` line). New items append cleanly to <Static>,
so the summary flows in naturally once the fast-model call resolves.
Compact mode still replaces the merged tool_group header with the
label and hides the standalone summary line via the `compactMode`
guard.

With this, the feature works under the default `ui.compactMode: false`
— not just the opt-in compact view.

* docs: tool-use-summaries feature guide, settings entry, and design doc

Three new docs matching the existing fast-model feature docs layout:

- docs/users/features/tool-use-summaries.md — user-facing guide
  covering full + compact rendering, configuration (settings + env),
  failure modes, cost, and cross-links to followup-suggestions.

- docs/users/configuration/settings.md — register the new
  experimental.emitToolUseSummaries setting next to the other
  fast-model-driven UI settings.

- docs/design/tool-use-summary/tool-use-summary-design.md — deep dive
  matching the compact-mode-design.md competitive-analysis style.
  Documents the Claude Code port (prompt, truncation, timing, gate),
  the deviations (settings layer, default on, cleanSummary, dual
  render paths), and the Ink <Static> append-only rationale that
  drove the inline full-mode render vs header-replacement split.

* docs: add Recommended pairing section to tool-use-summaries

Full-mode rendering of the summary works, but for small same-type
batches (Read × 3 and similar) the label visibly restates what the
tool lines already show. Pairing with ui.compactMode: true folds
the whole batch into a single labeled row, which is the cleanest
transcript shape once the label is available.

Adds a dedicated section showing the paired settings.json snippet
and explicitly calling out when each mode wins (and when to turn
the feature off instead).

* fix: address review feedback on tool-use summary generation

Addresses multiple issues from @chiga0's review:

Blocking — compact-mode label invisible for single-batch turns.
mergeCompactToolGroups's adjacency-only gating left a trailing
tool_use_summary in the merged result whenever there was no second
batch to merge across. That pushed mergedHistory.length lock-step
with history.length and MainContent's refreshStatic heuristic
(currMLen <= prevMLen) never fired, so Ink's append-only <Static>
never repainted the tool_group with its newly-looked-up label.
Drop tool_use_summary items unconditionally now; gemini_thought
still survives to avoid unnecessary repaints. New tests cover
the single-batch case and the summary-before-user-message case.

Blocking — stale summary appears after Ctrl+C on the next turn.
summarySignal captured the CURRENT turn's AbortController, but the
summary resolves during the NEXT turn's streaming window. The next
turn's submitQuery allocates a fresh controller, so the captured
signal was never aborted — Ctrl+C during the new turn used to let
the previous turn's summary land in the transcript seconds later.
Fix: dedicated per-batch AbortController tracked in a ref set,
aborted eagerly from cancelOngoingRequest; resolve-time check reads
the live abort state and turnCancelledRef.

High — summarizer input pollution.
geminiTools contained error/cancelled tools; retry-loop warnings
and "Cancelled by user" strings were feeding the fast model.
cleanSummary can only reject error-shaped output, not prevent the
model from hallucinating a plausible label from bad input (the PR's
own tmux screenshot showed "Read txt files · 5 tools" where 4 of
the 5 were prior-retry failures). Filter to status === 'success'
before building the prompt; skip the call entirely if nothing's
left.

High — unstable label on merged groups.
getCompactLabel iterated all callIds and returned the first hit,
so asynchronous resolution order made the header visibly flip
from SB to SA when batch A resolved after batch B. Lock onto
item.tools[0].callId to keep stable "leading batch governs"
semantics.

High — force-expanded groups in compact mode had no label at all.
Compact mode routes non-force-expand groups through
CompactToolGroupDisplay (consumes compactLabel) and force-expand
groups through the full ToolGroupMessage (ignores compactLabel);
the standalone ● line was gated on !compactMode, creating a dead
zone — exactly the diagnostically valuable case. MainContent now
computes absorbedCallIds (which groups actually consume the
header replacement) and passes summaryAbsorbed to
HistoryItemDisplay; force-expand groups in compact mode get the
standalone line as the label's only path to the screen.

Medium — cleanSummary robustness.
Extend quote-strip to Unicode curly + CJK corner brackets; strip
markdown emphasis (**bold**, _italic_); broaden refusal-prefix
rejection to curly-apostrophe "I can't", Chinese "我无法 / 我不能 /
抱歉 / 无法", and "Failed to / Sorry, / Request failed". 7 new
cleanSummary tests cover the added cases.

Low — concurrent-rendering safety.
Move historyRef.current = history from render phase into
useLayoutEffect so bailed renders can't leave a dropped value.

Low — CompactToolGroupDisplay readability.
Extract renderSummaryHeader / renderDefaultHeader helpers and
document the toolCalls.length > 1 count-suffix guard so a future
"fix" to >= 1 doesn't reintroduce "Read config.json · 1 tools".

Docs — add Scope & Lifecycle section to tool-use-summaries.md
covering (1) one generation per batch shared by both modes,
(2) no backfill on toggle / session resume, (3) main-agent batches
only with the Task-tool clarification.

* fix: address second-round review feedback on tool-use summaries

Critical — force-expand groups lost their summary entirely.
Previous round's "drop tool_use_summary unconditionally" merge fix
also stripped summaries for force-expanded groups, defeating the
exact case (errors, confirmations, focused shell) where the
standalone ● label is the label's only path to the screen. The
merge function now takes an absorbedCallIds set: summaries whose
preceding callIds are all absorbed by a compact tool_group header
are dropped (so refreshStatic still fires), but force-expanded
summaries pass through to be rendered standalone by
HistoryItemDisplay. MainContent computes absorbedCallIds from raw
history and passes it in. New tests cover both the absorbed-drop
and the force-expand-preserve cases plus the empty-set default
for callers that don't compute absorption.

Suggestion — late-arriving summaries could land out of order.
A slow fast-model call could resolve after the next turn's
content was committed, planting the ● label between later items
in full mode. The resolve callback now captures the first batch
callId, locates the corresponding tool_group at resolve time,
and drops the summary if a newer tool_group has already appeared
in history. New test exercises this with a manually-resolved
fast-model promise.

Suggestion — truncateJson allocated full JSON for large strings.
A 10MB ReadFile result was being JSON.stringify'd in full only to
be sliced down to 300 chars. Added preTruncate that walks the
value (depth-bounded to 4) and slices string leaves to maxLength
before serialization. Tests verify the input never reaches its
full pre-cap form.

Suggestion — settings description over-claimed SDK emission.
The description said summaries are emitted to SDK clients as a
tool_use_summary message; the SDK plumbing isn't actually wired
in this PR (the factory is exported for follow-up). Updated
settings.json description and regenerated the vscode schema to
state CLI-only scope explicitly.

Suggestion — fastModel data-boundary not documented.
When fastModel uses a different provider than the main session
model, tool inputs/outputs cross a new auth boundary that users
may not expect. Added "Data flow & privacy" section to the user
feature doc spelling out: same-provider fast model = no scope
change; different-provider = strictly larger sharing scope; two
escape hatches (same-provider fast model OR feature off).
Code-level mitigation (metadata-only mode) deferred.
2026-04-27 16:54:10 +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
Reid
d1c8dff4d2
feat(arena): add comparison summary for agent results (#3394)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
Adds a summary view that runs after Arena agents finish, so users can
compare model outputs without opening each agent's conversation first.

Summary surface:
- Agent status overview
- Files changed in common vs. unique to one agent
- Per-agent approach summary generated through that agent's own provider
- Token / runtime / line-change / file-count metrics

Selection dialog now supports:
- p — toggle preview for the highlighted agent
- d — toggle detailed diff
- Enter — select winner
- x — discard all results
- Esc — cancel

Approach summary generation:
- Each agent's summary is generated through that agent's own content
  generator, keeping mixed-provider Arena sessions within their
  respective auth boundaries
- 20s timeout + AbortController per agent, bounded prompt inputs
  (finalText 2K, transcript 6K, diff 6K)
- Falls back to a deterministic "Changed N files ..." summary when no
  per-agent generator is available or on error

Diff summary now handles binary, rename-only, and mode-only diffs;
the previous heuristic required textual +/- hunks and would have
dropped those.

Resolves #2559
2026-04-22 05:31:19 +08:00
zhangxy-zju
ebe364d0b8
feat(retry): add persistent retry mode for unattended CI/CD environments (#3080)
* feat(retry): add persistent retry mode for unattended CI/CD environments

When running in CI/CD pipelines or background daemon mode, transient API
capacity errors (429/529) should not terminate long-running tasks after a
fixed number of retries. This adds an environment-aware persistent retry
mode that retries indefinitely for transient errors, with exponential
backoff capped at 5 minutes and heartbeat keepalives every 30 seconds to
prevent CI runner timeouts.

* docs: add persistent retry mode documentation

Add environment variable entries (QWEN_CODE_UNATTENDED_RETRY, QWEN_CODE_BG)
to the settings reference, and a new "Persistent Retry Mode" section to the
headless mode docs covering activation, behavior, and CI/CD usage examples.

* refactor(retry): simplify to single explicit env var QWEN_CODE_UNATTENDED_RETRY

Remove QWEN_CODE_BG and CI=true as activation triggers for persistent retry.
Having multiple env vars with identical behavior adds confusion, and silently
activating infinite retry on CI=true is dangerous — a regular CI test hitting
a 429 would hang forever instead of failing fast.

* fix(retry): address PR review feedback

- Forward caller's abortSignal into retryWithBackoff in both
  baseLlmClient.ts and geminiChat.ts so persistent waits remain
  cancellable (wenshao)
- Re-apply maxBackoff and capMs after jitter so delays strictly
  respect stated caps (Copilot)
- Respect shouldRetryOnError in persistent mode so callers can
  force fast-fail even for transient 429/529 errors (Copilot)
- Guard sleepWithHeartbeat against infinite loop when heartbeat
  interval is <= 0 via Math.max(1, ...) (Copilot)
- Normalize isEnvTruthy with trim/toLowerCase for robust env
  var parsing across CI conventions (Copilot)

* test(retry): add missing UT for shouldRetryOnError override and heartbeat zero-interval guard

* fix(retry): do not cap Retry-After delays at maxBackoff

Server-specified Retry-After values should only be limited by the
absolute cap (capMs/6h), not the exponential backoff cap (maxBackoff/5min).
Jitter is also skipped for Retry-After since the server already specified
the exact wait time.

* refactor(retry): align isUnattendedMode with project env parsing convention

Replace custom isEnvTruthy (trim + toLowerCase) with strict matching
(val === 'true' || val === '1') to match parseBooleanEnvFlag used
elsewhere in the codebase. Prevents inconsistent behavior where
'TRUE' or ' 1 ' would activate persistent retry here but not in
telemetry or other env-driven features.

* test(retry): add Retry-After handling tests for persistent mode

Cover three key behaviors:
- Retry-After is NOT capped at maxBackoff (only at capMs)
- Retry-After IS capped at persistentCapMs absolute limit
- Retry-After delays have no jitter applied

* fix(test): add isUnattendedMode to retry.js mock in baseLlmClient tests

The existing vi.mock for retry.js only exported retryWithBackoff.
After adding isUnattendedMode to the retry module, baseLlmClient.ts
imports it, causing all 10 generateJson tests to fail with
'No "isUnattendedMode" export is defined on the mock'.

* fix(retry): wire persistent retry mode into client.ts generateContent

Forward persistentMode and abortSignal to retryWithBackoff() in
GeminiClient.generateContent(), matching the existing wiring in
baseLlmClient.ts and geminiChat.ts.
2026-04-21 22:08:11 +08:00
Shaojin Wen
52c7a3d0ed
fix(cli): pin /recap above input and align defaults with fastModel (#3478)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(cli): pin /recap above input box and align defaults with fastModel

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

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

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

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

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

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

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

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

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

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

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

Two issues from review:

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

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

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

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

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

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

Aligns settings.md, design doc, and the regenerated JSON schema.
2026-04-20 23:58:19 +08:00
Shaojin Wen
c74d7678cb
Revert "feat(core): add dynamic swarm worker tool (#3433)" (#3468)
This reverts commit f7ebc372f1.
2026-04-20 16:40:14 +08:00
Edenman
6c999fe29f
feat(cli): add OAuth configuration flags to mcp add (#3442)
* feat(cli): Add OAuth redirect URI support to  command

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

Fixes #3336

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

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

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

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

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-20 14:12:17 +08:00
Shaojin Wen
60a6dfc14c
feat(cli): add session recap with /recap and auto-show on return (#3434)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(cli): add session recap with /recap and auto-show on return

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

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

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

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

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

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

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

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

Two related issues from review:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Return void on the interactive success path and on the abort path so
the processor's `if (result)` check skips the message-handler branch
entirely. Widen the action's return type to `void | SlashCommandActionReturn`
to match (same shape as btwCommand).
2026-04-19 21:38:48 +08:00
Reid
f7ebc372f1
feat(core): add dynamic swarm worker tool (#3433)
* feat(core): add dynamic swarm worker tool

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

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

* fix(core): harden swarm worker execution

  Prevent swarm calls from bypassing the outer scheduler concurrency budget.

  Disallow interactive question prompts in swarm workers by default, and avoid incomplete Markdown table escaping by using an HTML entity for
  pipe characters. Add focused tests for the scheduler behavior, worker tool restrictions, and result formatting.
2026-04-19 14:46:59 +08:00
Shaojin Wen
4bf5bf22de
feat(cli): support refreshInterval in statusLine for periodic refresh (#3383)
* feat(cli): support refreshInterval in statusLine for periodic refresh

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Keeping the output-dedup commit (e1d321186) — that one is a strict
improvement regardless of the underlying Ink behavior. Will request
BZ-D's specific terminal setup and reopen with a verified fix (or
confirm the issue is specific to a particular emulator, not flex/Ink).
2026-04-19 11:12:16 +08:00
ChiGao
9e26424aa7
feat(cli): add dual-output sidecar mode for TUI (#3352)
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 dual-output sidecar mode for TUI

Adds an optional **dual-output** mode for the interactive TUI: while Qwen
Code keeps rendering normally on stdout, it concurrently emits a structured
JSON event stream on a second channel (--json-fd / --json-file) and
optionally watches a JSONL command file (--input-file) for prompts and
tool-permission responses written by an external program.

This unlocks programmatic embedding of the TUI from IDE extensions, web
frontends, CI agents, or automation scripts without forcing them to give
up the rich interactive UI in favor of --output-format=stream-json.

## Design

The TUI already has a battle-tested JSON event emitter
(`StreamJsonOutputAdapter`). This change makes that adapter pluggable on
its output stream and wires a small `DualOutputBridge` that forwards TUI
events to a second instance of the adapter writing to fd / file.

For tool approvals, when a tool enters awaiting_approval the bridge emits
`control_request` (subtype `can_use_tool`); whichever side resolves first
(TUI's native UI or `confirmation_response` via --input-file) wins, and a
`control_response` is mirrored back so all observers stay in sync.

`session_start` is announced once when the bridge is constructed so
consumers can correlate the channel with a session before any other event
arrives.

## CLI surface

- `--json-fd <n>` — write JSON events to fd n (n >= 3; provided via spawn
  stdio).
- `--json-file <path>` — write JSON events to a file / FIFO / /dev/fd/N.
- `--input-file <path>` — watch this file for JSONL commands.

`--json-fd` and `--json-file` are mutually exclusive. fds 0/1/2 are
rejected to prevent corrupting the TUI.

## Wire protocol

Output: existing stream-json schema with `includePartialMessages` always
enabled, plus:

- `system` / `subtype: session_start` — emitted once on bridge
  construction.
- `control_request` / `subtype: can_use_tool` — pending tool approval.
- `control_response` — final approval outcome (mirrors TUI-native or
  external resolution).

Input (--input-file):

    {"type":"submit","text":"What does this function do?"}
    {"type":"confirmation_response","request_id":"...","allowed":true}

`submit` is queued and retried when the TUI returns to idle.
`confirmation_response` is dispatched immediately — a pending tool call
is blocking and the response cannot wait behind earlier submits.

See `docs/users/features/dual-output.md` for the full schema, latency
notes, failure modes, and a spawn example.

## What changes when the flags are absent

Nothing. The bridge and watcher are constructed only when the relevant
flags are set; otherwise the React Context providers carry `null` and
every callsite short-circuits. No overhead, no behavioral change for
existing users.

## Failure handling

- Bad fd / unopenable path → warning on stderr, dual output stays
  disabled, TUI launches normally.
- Consumer disconnect (EPIPE) → bridge silently disables itself, TUI
  keeps running.
- Any exception inside the adapter → caught, logged, bridge disabled.
  The TUI is never crashed by a dual-output failure.

## Files

New:
- packages/cli/src/dualOutput/{DualOutputBridge,DualOutputContext,index}.{ts,tsx}
- packages/cli/src/remoteInput/{RemoteInputWatcher,RemoteInputContext,index}.{ts,tsx}
- packages/cli/src/nonInteractive/io/index.ts
- docs/users/features/dual-output.md

Modified:
- packages/core/src/config/config.ts — 3 new ConfigParameters fields + getters
- packages/cli/src/config/config.ts — yargs options + mutex validation
- packages/cli/src/gemini.tsx — instantiate bridge / watcher in
  startInteractiveUI, wrap with Context Providers, register cleanup
- packages/cli/src/ui/AppContainer.tsx — connect RemoteInput to
  submitQuery, bridge tool confirmations
- packages/cli/src/ui/hooks/useGeminiStream.ts — call
  dualOutput?.processEvent(...) at five existing event points
- packages/cli/src/nonInteractive/io/{Base,Stream}JsonOutputAdapter.ts —
  StreamJsonOutputAdapter accepts an injected output stream; base adapter
  exposes emitPermissionRequest / emitControlResponse through a new
  emitControlMessageImpl hook (default no-op in batch mode).

## Tests

- packages/cli/src/dualOutput/DualOutputBridge.test.ts — fd validation,
  auto session_start, control-event routing, post-shutdown safety.
- packages/cli/src/remoteInput/RemoteInputWatcher.test.ts — submit
  forwarding, immediate confirmation dispatch, busy/idle retry,
  malformed-line tolerance, shutdown.
- packages/cli/src/nonInteractive/io/StreamJsonOutputAdapter.dualOutput.test.ts —
  custom outputStream injection and new emitPermissionRequest /
  emitControlResponse paths.

tsc --noEmit -p packages/cli/tsconfig.json is clean.
vitest run src/nonInteractive src/dualOutput src/remoteInput → 297 passed,
1 skipped, 11 files.

* feat(cli): dual-output capability handshake, session_end, control_error, settings.json

Incremental improvements on top of the initial dual-output PR based on
reviewer feedback. All extensions are additive; older consumers that
ignore unknown fields keep working.

## Capability handshake in session_start

`session_start.data` now carries three new fields so consumers can
feature-detect without sniffing the stream:

- `protocol_version` (integer, currently 1) — bumped on any protocol
  change consumers might care about.
- `version` (string) — the Qwen Code CLI version, threaded in from
  `gemini.tsx`.
- `supported_events` (string[]) — the event kinds this bridge version
  is known to emit, exported as `SUPPORTED_EVENTS` from the module.

## session_end on bridge shutdown

DualOutputBridge.shutdown() now emits a final
`system` / `session_end` event carrying `session_id` before closing the
stream. Gives consumers a definitive termination signal rather than
requiring them to infer it from EPIPE. Idempotent — calling shutdown
twice emits exactly one session_end.

## control_error emission path

`ControlErrorResponse` (already defined in types.ts) now has a first-
class emission path: `BaseJsonOutputAdapter.emitControlError(requestId,
message)` → `control_response` with `subtype: 'error'`. Wired into
AppContainer's remote-input confirmation handler so that a
`confirmation_response` referencing an unknown / already-resolved
request_id produces a structured error reply instead of silently
dropping, letting consumers retry or surface the error.

## settings.json support

New `dualOutput` top-level settings block with `jsonFile` and
`inputFile` properties. `--json-fd` has no settings equivalent (fd
passing is a spawn-time concern). CLI flag wins over settings when
both are present, so scripted one-off runs still work unchanged.
`requiresRestart: true` since the bridge is constructed once at
startup.

## Documentation

`docs/users/features/dual-output.md` gains three major sections:

- **Use cases** — concrete integration scenarios (terminal+chat dual
  sync, IDE extensions, web frontends, CI observers, multi-agent
  orchestration, session replay, observability, QA).
- **Why two output flags?** — detailed rationale for coexisting
  `--json-fd` and `--json-file`, including the PTY constraint
  (`node-pty` / `bun-pty` expose no stdio array, and `forkpty(3)` /
  `login_tty` actively close fds >= 3 before exec).
- **Comparison with Claude Code's stream-json** — schema-parity
  matrix, transport-topology differences, permission-control-plane
  behavioral notes, and a "room to improve" section as a design
  horizon.
- **Runnable demos** — seven copy-paste POCs: event observer, remote
  submit, permission bridge, Node embedder with capability
  feature-detection, session_end handling, failure drills.
- **Settings-based configuration** — example settings.json snippet and
  precedence rules.

## Tests

- DualOutputBridge.test.ts: new cases for capability handshake shape,
  session_end on shutdown, shutdown idempotency, and emitControlError.
- StreamJsonOutputAdapter.dualOutput.test.ts: new case for
  emitControlError at the adapter level.

302 passed, 1 skipped, 11 files. tsc --noEmit -p packages/cli is clean.

* docs(dual-output): shrink Claude Code comparison to one honest sentence

After actually reading the Claude Code source (src/cli/structuredIO.ts,
src/bridge/*, src/utils/messages/systemInit.ts), the previous
"Comparison with Claude Code's stream-json" section was overstated:

- Claude Code has no equivalent of TUI + sidecar running simultaneously.
  Its stream-json only works with --print (non-interactive); the bridge
  in src/bridge/* is Anthropic's own remote worker protocol, not a
  local embedding surface.
- CC uses `system/init` (not `session_start`) and has no session_end in
  the wire protocol, so the schema-parity table contained false ticks.
- Framing this PR as "parity with Claude Code" is therefore inaccurate;
  it's filling a gap Claude Code does not address.

Replace the whole multi-section comparison (schema matrix, transport
table, permission notes, borrow list, roadmap) with a single sentence
stating the accurate relation: same event format in spirit, different
topology — CC's is non-interactive only.

* fix(cli): address review feedback on dual-output sidecar mode

- Fix control_response mirror: external-initiated confirmations now
  emit control_response via the same mirror useEffect as TUI-native
  resolutions, making the emission path symmetric for all observers.
- Fix ENOENT: --json-file with a non-existent path now falls back to
  createWriteStream (auto-creates the file) instead of throwing.
- Fix race: add reading guard to RemoteInputWatcher.readNewLines()
  preventing duplicate command processing on rapid appends.
- Refactor confirmationHandler to use refs (pendingToolCallsRef,
  dualOutputRef) and register once (deps: [remoteInput]) to eliminate
  teardown/re-registration churn.
- Add debug logging to shutdown bare catch for ops correlation.
- Add ENOENT fallback test case for DualOutputBridge.
- Regenerate settings.schema.json for dualOutput section.

Generated with AI

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

* fix(cli): make RemoteInputWatcher poll interval configurable for CI reliability

RemoteInputWatcher.test.ts was timing out in CI (5s default) because
fs.watchFile's 500ms poll interval is unreliable under load. Fix:

- Accept optional `pollIntervalMs` in constructor (default 500ms).
- Tests use 100ms poll interval for faster feedback.
- Increase per-test timeout to 15s and waitFor timeout to 10s.
- Increase "TUI busy" wait from 800ms to 1500ms for CI headroom.

Generated with AI

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

* fix(cli): eliminate fs.watchFile timing dependency in RemoteInputWatcher tests

Tests were flaky across all CI platforms (macOS/ubuntu/windows) because
fs.watchFile polling (even at 100ms) is unreliable under CI load.

Fix: expose checkForNewInput() as a public method that directly triggers
file reading and returns a Promise. Tests now call it synchronously after
writing to the input file — no polling, no timeouts, deterministic.

Also fixes:
- Windows ENOTEMPTY: add delay in afterEach before rmSync
- Add active check in readNewLines to respect shutdown state
- readNewLines now returns Promise<void> for awaitable reads

Generated with AI

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

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-18 02:14:53 +08:00
joeytoday
89167618d8
docs: update authentication methods to reflect OAuth discontinuation (#3325)
* docs: update authentication methods to reflect OAuth discontinuation

Remove deprecated Qwen OAuth references and update documentation to
direct users to valid authentication methods (API Key, Coding Plan,
or Local Inference) following the OAuth free tier discontinuation on
2026-04-15.

Closes #3316

* docs: fix quickstart auth description to match actual /auth UI

The /auth command shows three options: Alibaba Cloud Coding Plan,
API Key, and Qwen OAuth (discontinued). Updated quickstart.md to
accurately reflect this UI instead of splitting into Option A/B/C.

Also updated settings.md, commands.md, and troubleshooting.md with
minor OAuth-related cleanups.

* docs: update .qwen workspace description in quickstart

Remove reference to 'Qwen account' since OAuth is discontinued.
The .qwen directory is created by Qwen Code itself for storing
credentials, configuration, and session data.

* docs: fix warning block formatting in quickstart

- Add missing '>' continuation for the OAuth discontinuation warning block

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

* docs: update README Qwen3.6-Plus description

- Remove mention of running Qwen3.6-Plus locally via Ollama/vLLM
- Keep only the Alibaba Cloud ModelStudio API key option

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

* docs: address review feedback - remove Local Inference from auth, add dual-region links

- Local Inference removed from auth method lists, kept as separate
  'Local Model Setup' section with detailed Ollama/vLLM config examples
- All links now provide dual-region URLs (Beijing + intl)
- .qwen workspace note restored to original meaning (cost tracking)
- Device auth flow error kept scoped to legacy OAuth
- API setup guide links updated with confirmed intl URL

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-17 15:34:18 +08:00
Shaojin Wen
b004450d7f
feat(cli): support multi-line status line output (#3311)
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): support multi-line status line output (#3211)

Remove the single-line hard limit (.split('\n')[0]) from the status line
hook so user scripts can output multiple rows. Footer renders each line
as a separate <Text wrap="truncate"> element, preserving per-line
horizontal truncation. Ink's virtual DOM handles re-rendering without
manual ANSI cursor management.

* feat(cli): cap status line output at 3 lines

Prevent runaway scripts from flooding the footer — lines beyond the
third are silently discarded.

* docs: mention 3-line cap in status line docs and agent prompt

* fix(cli): cap status line at 2 lines to keep footer within 3 rows

Footer has a fixed bottom row (hint/mode indicator), so status line
gets at most 2 lines to keep the total footer height at 3 rows max.

* test(cli): improve useStatusLine coverage to 100% lines

Add tests for: per-model metrics payload, contextWindowSize/version/
model fallbacks, config removal with pending debounce, command change
cancelling pending debounce.

* docs: update status line ASCII diagram for multi-line layouts

Also fix TS error in test (null → null as never for mock return).

* refactor(cli): return string[] from useStatusLine, filter empty lines

Address review feedback:
- Hook returns `lines: string[]` instead of `text: string | null`,
  eliminating the join/split round-trip with Footer.
- Filter empty lines before slicing so leading blanks don't eat real
  content (e.g. "\n\nreal content" no longer yields ["", ""]).
- Export MAX_STATUS_LINES with comment explaining the 3-row constraint.
- Use `status-line-${i}` as React key for clarity.

* test(cli): add Footer multi-line rendering, \r\n, and pure-newline tests

Address remaining review feedback:
- Footer test: mock useStatusLine, verify multi-line rendering and
  hint suppression.
- useStatusLine test: add \r\n line ending and pure-newline edge case.

* fix(cli): align right footer indicators to top

When the status line has multiple rows, the left column becomes taller
than the right section. The outer Box defaults to `alignItems: stretch`
which caused the indicators to visually center; add `alignItems="flex-start"`
on the right Box so they stay anchored to the top row.

Reported via e2e test in #3311.
2026-04-17 12:44:30 +08:00
顾盼
9e2f63a1ca
feat(memory): managed auto-memory and auto-dream system (#3087)
* docs: add auto-memory implementation log

* feat(core): add managed auto-memory storage scaffold

* feat(core): load managed auto-memory index

* feat(core): add managed auto-memory recall

* feat(core): add managed auto-memory extraction

* feat(cli): add managed auto-memory dream commands

* feat(core): add auxiliary side-query foundation

* feat(memory): add model-driven recall selection

* feat(memory): add model-driven extraction planner

* feat(core): add background task runtime foundation

* feat(memory): schedule auto dream in background

* feat(core): add background agent runner foundation

* feat(memory): add extraction agent planner

* feat(core): add dream agent planner

* feat(core): rebuild managed memory index

* feat(memory): add governance status commands

* feat(memory): add managed forget flow

* feat(core): harden background agent planning

* feat(memory): complete managed parity closure

* test(memory): add managed lifecycle integration coverage

* feat: same to cc

* feat(memory-ui): add memory saved notification and memory count badge

Feature 3 - Memory Saved Notification:
- Add HistoryItemMemorySaved type to types.ts
- Create MemorySavedMessage component for rendering '● Saved/Updated N memories'
- In useGeminiStream: detect in-turn memory writes via mapToDisplay's
  memoryWriteCount field and emit 'memory_saved' history item after turn
- In client.ts: capture background dream/extract promises and expose
  via consumePendingMemoryTaskPromises(); useGeminiStream listens
  post-turn and emits 'Updated N memories' notification for background tasks

Feature 4 - Memory Count Badge:
- Add isMemoryOp field to IndividualToolCallDisplay
- Add memoryWriteCount/memoryReadCount to HistoryItemToolGroup
- Add detectMemoryOp() in useReactToolScheduler using isAutoMemPath
- ToolGroupMessage renders '● Recalled N memories, Wrote N memories' badge
  at the top of tool groups that touch memory files

Fix: process.env bracket-access in paths.ts (noPropertyAccessFromIndexSignature)
Fix: MemoryDialog.test.tsx mock useSettings to satisfy SettingsProvider requirement

* fix(memory-ui): auto-approve memory writes, collapse memory tool groups, fix MEMORY.md path

Problem 1 - Auto-approve memory file operations:
- write-file.ts: getDefaultPermission() checks isAutoMemPath; returns 'allow'
  for managed auto-memory files, 'ask' for all other files
- edit.ts: same pattern

Problem 2 - Feature 4 UX: collapse memory-only tool groups:
- ToolGroupMessage: detect when all tool calls have isMemoryOp set (pure memory
  group) and all are complete; render compact '● Recalled/Wrote N memories
  (ctrl+o to expand)' instead of individual tool call rows
- ctrl+o toggles expand/collapse when isFocused and group is memory-only
- Mixed groups (memory + other tools) keep badge-at-top behaviour
- Expanded state shows individual tool calls with '● Memory operations
  (ctrl+o to collapse)' header

Problem 3 - MEMORY.md path mismatch:
- prompt.ts: Step 2 now references full absolute path ${memoryDir}/MEMORY.md
  so the model writes to the correct location inside the memory directory,
  not to the parent project directory

Fix tests:
- write-file.test.ts: add getProjectRoot to mockConfigInternal
- prompt.test.ts: update assertion to match full-path section header

* fix(memory-ui): fix duplicate notification, broken ctrl+o, and Edit tool detection

- Remove duplicate 'Saved N memories' notification: the tool group badge already
  shows 'Wrote N memories'; the separate HistoryItemMemorySaved addItem after
  onComplete was double-counting. Keep only the background-task path
  (consumePendingMemoryTaskPromises).

- Remove ctrl+o expand: Ink's Static area freezes items on first render and
  cannot respond to user input. useInput/useState(isExpanded) in a Static item
  is a no-op. Removed the dead code; memory-only groups now always render as
  the compact summary (no fake interactive hint).

- Fix Edit tool detection: detectMemoryOp was checking for 'edit_file' but the
  real tool name constant is 'edit'. Also removed non-existent 'create_file'
  (write_file covers all writes). Now editing MEMORY.md is correctly identified
  as a memory write op, collapses to 'Wrote N memories', and is auto-approved.

* fix(dream): run /dream as a visible submit_prompt turn, not a silent background agent

The previous implementation ran an AgentHeadless background agent that could
take 5+ minutes with zero UI feedback — user saw a blank screen for the entire
duration and then at most one line of text.

Fix: /dream now returns submit_prompt with the consolidation task prompt so it
runs as a regular AI conversation turn. Tool calls (read_file, write_file, edit,
grep_search, list_directory, glob) are immediately visible as collapsed tool
groups as the model works through the memory files — identical UX to Claude Code.

Also export buildConsolidationTaskPrompt from dreamAgentPlanner so dreamCommand
can reuse the same detailed consolidation prompt that was already written.

* fix(memory): auto-allow ls/glob/grep on memory base directory

Add getMemoryBaseDir() to getDefaultPermission() allow list in ls.ts,
glob.ts, and grep.ts — mirrors the existing pattern in read-file.ts.

Without this, ListFiles/Glob/Grep on ~/.qwen/* would trigger an
approval dialog, blocking /dream at its very first step.

* fix(background): prevent permission prompt hangs in background agents

Match Claude Code's headless-agent intent: background memory agents must never
block on interactive permission prompts.

Wrap background runtime config so getApprovalMode() returns YOLO, ensuring any
ask decision is auto-approved instead of hanging forever. Add regression test
covering the wrapped approval mode.

* fix(memory): run auto extract through forked agent

Make managed auto-memory extraction follow the Claude Code architecture:
background extraction now uses a forked agent to read/write memory files
directly, instead of planning patches and applying them with a separate
filesystem pipeline.

Keep the old patch/model path only as fallback if the forked agent fails.
Add regression tests covering the new execution path and tool whitelist.

* refactor(memory): remove legacy extract fallback pipeline

Delete the old patch/model/heuristic extraction path entirely.
Managed auto-memory extract now runs only through the forked-agent
execution flow, with no planner/apply fallback stages remaining.

Also remove obsolete exports/tests and update scheduler/integration
coverage to use the forked-agent-only architecture.

* refactor(memory): move auxiliary files out of memory/ directory

meta.json, extract-cursor.json, and consolidation.lock are internal
bookkeeping files, not user-visible memories. Move them one level up
to the project state dir (parent of memory/) so that the memory/
directory contains only MEMORY.md and topic files, matching the
clean layout of the upstream reference implementation.

Add getAutoMemoryProjectStateDir() helper in paths.ts and update the
three path accessors + store.test.ts path assertions accordingly.

* fix(memory): record lastDreamAt after manual /dream run

The /dream command submits a prompt to the main agent (submit_prompt),
which writes memory files directly. Because it bypasses dreamScheduler,
meta.json was never updated and /memory always showed 'never'.

Fix by:
- Exporting writeDreamManualRunToMetadata() from dream.ts
- Adding optional onComplete callback to SubmitPromptActionReturn and
  SubmitPromptResult (types.ts / commands/types.ts)
- Propagating onComplete through slashCommandProcessor.ts
- Firing onComplete after turn completion in useGeminiStream.ts
- Providing the callback in dreamCommand.ts to write lastDreamAt

* fix(memory): remove scope params from /remember in managed auto-memory mode

--global/--project are legacy save_memory tool concepts. In managed
auto-memory mode the forked agent decides the appropriate type
(user/feedback/project/reference) based on the content of the fact.

Also improve the prompt wording to explicitly ask the agent to choose
the correct type, reducing the tendency to default to 'project'.

* feat(ui): show '✦ dreaming' indicator in footer during background dream

Subscribe to getManagedAutoMemoryDreamTaskRegistry() in Footer via a
useDreamRunning() hook. While any dream task for the current project is
pending or running, display '✦ dreaming' in the right section of the
footer bar, between Debug Mode and context usage.

* refactor(memory): align dream/extract infrastructure with Claude Code patterns

Five improvements based on Claude Code parity audit:

1. Memoize getAutoMemoryRoot (paths.ts)
   - Add _autoMemoryRootCache Map, keyed by projectRoot
   - findCanonicalGitRoot() walks the filesystem per call; memoize avoids
     repeated git-tree traversal on hot-path schedulers/scanners
   - Expose clearAutoMemoryRootCache() for test teardown

2. Lock file stores PID + isProcessRunning reclaim (dreamScheduler.ts)
   - acquireDreamLock() writes process.pid to the lock file body
   - lockExists() reads PID and calls process.kill(pid, 0); dead/missing
     PID reclaims the lock immediately instead of waiting 2h
   - Stale threshold reduced to 1h (PID-reuse guard, same as CC)

3. Session scan throttle (dreamScheduler.ts)
   - Add SESSION_SCAN_INTERVAL_MS = 10min (same as CC)
   - Add lastSessionScanAt Map<projectRoot, number> to ManagedAutoMemoryDreamRuntime
   - When time-gate passes but session-gate doesn't, throttle prevents
     re-scanning the filesystem on every user turn

4. mtime-based session counting (dreamScheduler.ts)
   - Replace fragile recentSessionIdsSinceDream Set in meta.json with
     filesystem mtime scan (listSessionsTouchedSince)
   - Mirrors Claude Code's listSessionsTouchedSince: reads session JSONL
     files from Storage.getProjectDir()/chats/, filters by mtime > lastDreamAt
   - Immune to meta.json corruption/loss; no per-turn metadata write
   - ManagedAutoMemoryDreamRuntime accepts injectable SessionScannerFn
     for clean unit testing without real session files

5. Extraction mutual exclusion extended to write_file/edit (extractScheduler.ts)
   - historySliceUsesMemoryTool() now checks write_file/edit/replace/create_file
     tool calls whose file_path is within isAutoMemPath()
   - Previously only detected save_memory; missed direct file writes by
     the main agent, causing redundant background extraction

* docs(memory): add user-facing memory docs, i18n for all locales, simplify /forget

- Add docs/users/features/memory.md: comprehensive user-facing guide covering
  QWEN.md instructions, auto-memory behaviour, all memory commands, and
  troubleshooting; replaces the placeholder auto-memory.md
- Update docs/users/features/_meta.ts: rename entry auto-memory → memory
- Update docs/users/features/commands.md: add /init, /remember, /forget,
  /dream rows; fix /memory description; remove /init duplicate
- Update docs/users/configuration/settings.md: add memory.* settings section
  (enableManagedAutoMemory, enableManagedAutoDream) between tools and permissions
- Remove /forget --apply flag: preview-then-apply flow replaced with direct
  deletion; update forgetCommand.ts, en.js, zh.js accordingly
- Add all auto-memory i18n keys to de, ja, pt, ru locales (18 keys each):
  Open auto-memory folder, Auto-memory/Auto-dream status lines, never/on/off,
  ✦ dreaming, /forget and /remember usage strings, all managed-memory messages
- Remove dead save_memory branch from extractScheduler.partWritesToMemory()
- Add ✦ dreaming indicator to Footer.tsx with i18n; fix Footer.test.tsx mocks
- Refactor MemoryDialog.tsx auto-dream status line to use i18n
- Remove save_memory tool (memoryTool.ts/test); clean up webui references
- Add extractionPlanner.ts, const.ts and associated tests
- Delete stale docs/users/configuration/memory.md and
  docs/developers/tools/memory.md (content superseded)

* refactor(memory): remove all Claude Code references from comments and test names

* test(memory): remove empty placeholder test files that cause vitest to fail

* fix eslint

* fix test in windows

* fix test

* fix(memory): address critical review findings from PR #3087

- fix(read-file): narrow auto-allow from getMemoryBaseDir() (~/.qwen) to
  isAutoMemPath(projectRoot) to prevent exposing settings.json / OAuth
  credentials without user approval (wenshao review)

- fix(forget): per-entry deletion instead of whole-file unlink
  - assign stable per-entry IDs (relativePath:index for multi-entry files)
    so the model can target individual entries without removing siblings
  - rewrite file keeping unmatched entries; only unlink when file becomes
    empty (wenshao review)

- fix(entries): round-trip correctness for multi-entry new-format bodies
  - parseAutoMemoryEntries: plain-text line closes current entry and opens
    a new one (was silently ignored when current was already set)
  - renderAutoMemoryBody: emit blank line between adjacent entries so the
    parser can detect entry boundaries on re-read (wenshao review)

- fix(entries): resolve two CodeQL polynomial-regex alerts
  - indentedMatch: \s{2,}(?:[-*]\s+)? → [\t ]{2,}(?:[-*][\t ]+)?
  - topLevelMatch: :\s*(.+)$ → :[ \t]*(\S.*)$
  (github-advanced-security review)

- fix(scan.test): use forward-slash literal for relativePath expectation
  since listMarkdownFiles() normalises all separators to '/' on all
  platforms including Windows

* fix(memory): replace isAutoMemPath startsWith with path.relative()

Using path.relative() instead of string startsWith() is more robust
across platforms — it correctly handles Windows path-separator
differences and avoids potential edge cases where a path prefix match
could succeed on non-separator boundaries.

Addresses github-actions review item 3 (PR #3087).

* feat(telemetry): add auto-memory telemetry instrumentation

Add OpenTelemetry logs + metrics for the five auto-memory lifecycle
events: extract, dream, recall, forget, and remember.

Telemetry layer (packages/core/src/telemetry/):
- constants.ts: 5 new event-name constants
  (qwen-code.memory.{extract,dream,recall,forget,remember})
- types.ts: 5 new event classes with typed constructor params
  (MemoryExtractEvent, MemoryDreamEvent, MemoryRecallEvent,
   MemoryForgetEvent, MemoryRememberEvent)
- metrics.ts: 8 new OTel instruments (5 Counters + 3 Histograms)
  with recordMemoryXxx() helpers; registered inside initializeMetrics()
- loggers.ts: logMemoryExtract/Dream/Recall/Forget/Remember() — each
  emits a structured log record and calls its recordXxx() counterpart
- index.ts: re-exports all new symbols

Instrumentation call-sites:
- extractScheduler.ts ManagedAutoMemoryExtractRuntime.runTask():
  emits extract event with trigger=auto, completed/failed status,
  patches_count, touched_topics, and wall-clock duration
- dream.ts runManagedAutoMemoryDream():
  emits dream event with trigger=auto, updated/noop status,
  deduped_entries, touched_topics, and duration; covers both
  agent-planner and mechanical fallback paths
- recall.ts resolveRelevantAutoMemoryPromptForQuery():
  emits recall event with strategy, docs_scanned/selected, and
  duration; covers model, heuristic, and none paths
- forget.ts forgetManagedAutoMemoryEntries():
  emits forget event with removed_entries_count, touched_topics,
  and selection_strategy (model/heuristic/none)
- rememberCommand.ts action():
  emits remember event with topic=managed|legacy at command
  invocation time (before agent decides the actual memory type)

* refactor(telemetry): remove memory forget/remember telemetry events

Remove EVENT_MEMORY_FORGET and EVENT_MEMORY_REMEMBER along with all
associated infrastructure that is no longer needed:

- constants.ts: remove EVENT_MEMORY_FORGET, EVENT_MEMORY_REMEMBER
- types.ts: remove MemoryForgetEvent, MemoryRememberEvent classes
- metrics.ts: remove MEMORY_FORGET_COUNT, MEMORY_REMEMBER_COUNT constants,
  memoryForgetCounter, memoryRememberCounter module vars,
  their initialization in initializeMetrics(), and
  recordMemoryForgetMetrics(), recordMemoryRememberMetrics() functions
- loggers.ts: remove logMemoryForget(), logMemoryRemember() functions
  and their imports
- index.ts: remove all re-exports for the above symbols
- memory/forget.ts: remove logMemoryForget call-site and import
- cli/rememberCommand.ts: remove logMemoryRemember call-sites and import

* change default value

* fix forked agent

* refactor(background): unify fork primitives into runForkedAgent + cleanup

- Merge runForkedQuery into runForkedAgent via TypeScript overloads:
  with cacheSafeParams → GeminiChat single-turn path (ForkedQueryResult)
  without cacheSafeParams → AgentHeadless multi-turn path (ForkedAgentResult)
- Delete forkedQuery.ts; move its test to background/forkedAgent.cache.test.ts
- Remove forkedQuery export from followup/index.ts
- Migrate all callers (suggestionGenerator, speculation, btwCommand, client)
  to import from background/forkedAgent
- Add getFastModel() / setFastModel() to Config; expose in CLI config init
  and ModelDialog / modelCommand
- Remove resolveFastModel() from AppContainer — now delegated to config.getFastModel()
- Strip Claude Code references from code comments

* fix(memory): address wenshao's critical review findings

- dream.ts: writeDreamManualRunToMetadata now persists lastDreamSessionId
  and resets recentSessionIdsSinceDream, preventing auto-dream from firing
  again in the same session after a manual /dream
- config.ts: gate managed auto-memory injection on getManagedAutoMemoryEnabled();
  when disabled, previously saved memories are no longer injected into new sessions
- rememberCommand.ts: remove legacy save_memory branch (tool was removed);
  fall back to submit_prompt directing agent to write to QWEN.md instead
- BuiltinCommandLoader.ts: only register /dream and /forget when managed
  auto-memory is enabled, matching the feature's runtime availability
- forget.ts: return early in forgetManagedAutoMemoryMatches when matches is
  empty, avoiding unnecessary directory scaffolding as a side effect

* fix test

* fix ci test

* feat(memory): align extract/dream agents to Claude Code patterns

- fix(client): move saveCacheSafeParams before early-return paths so
  extract agents always have cache params available (fixes extract never
  triggering in skipNextSpeakerCheck mode)

- feat(extract): add read-only shell tool + memory-scoped write
  permissions; create inline createMemoryScopedAgentConfig() with
  PermissionManager wrapper (isToolEnabled + evaluate) that allows only
  read-only shell commands and write/edit within the auto-memory dir

- feat(extract): align prompt to Claude Code patterns — manifest block
  listing existing files, parallel read-then-write strategy, two-step
  save (memory file then index)

- feat(dream): remove mechanical fallback; runManagedAutoMemoryDream is
  now agent-only and throws without config

- feat(dream): align prompt to Claude Code 4-phase structure
  (Orient/Gather/Consolidate/Prune+Index); add narrow transcript grep,
  relative→absolute date conversion, stale index pruning, index size cap

- fix(permissions): add isToolEnabled() to MemoryScopedPermissionManager
  to prevent TypeError crash in CoreToolScheduler._schedule

- test: update dreamScheduler tests to mock dream.js; replace removed
  mechanical-dedup test with scheduler infrastructure verification

* move doc to design

* refactor(memory): unify extract+dream background task management into MemoryBackgroundTaskHub

- Add memoryTaskHub.ts: single BackgroundTaskRegistry + BackgroundTaskDrainer shared
  by all memory background tasks; exposes listExtractTasks() / listDreamTasks()
  typed query helpers and a unified drain() method
- extractScheduler: ManagedAutoMemoryExtractRuntime accepts hub via constructor
  (defaults to defaultMemoryTaskHub); test factory gets isolated fresh hub
- dreamScheduler: same pattern — sessionScanner + hub injection; BackgroundTask-
  Scheduler initialized from injected hub; test factory gets isolated hub
- status.ts: replace two separate getRegistry() calls with defaultMemoryTaskHub
  typed query methods
- Footer.tsx (useDreamRunning): subscribe to shared registry, filter by
  DREAM_TASK_TYPE so extract tasks do not trigger the dream spinner
- index.ts: re-export memoryTaskHub.ts so defaultMemoryTaskHub/DREAM_TASK_TYPE/
  EXTRACT_TASK_TYPE are available as top-level package exports

* refactor(background): introduce general-purpose BackgroundTaskHub

Replace memory-specific MemoryBackgroundTaskHub with a domain-agnostic
BackgroundTaskHub in the background/ layer. Any future background task
runtime (3rd, 4th, …) plugs in by accepting a hub via constructor
injection — no new infrastructure required.

Changes:
- Add background/taskHub.ts: BackgroundTaskHub (registry + drainer +
  createScheduler() + listByType(taskType, projectRoot?)) and the
  globalBackgroundTaskHub singleton. Zero knowledge of any task type.
- Delete memory/memoryTaskHub.ts: its narrow listExtractTasks /
  listDreamTasks helpers are replaced by the generic listByType() call.
- Move EXTRACT_TASK_TYPE to extractScheduler.ts (owned by the runtime
  that defines it); replace 3 hardcoded string literals with the const.
- Move DREAM_TASK_TYPE to dreamScheduler.ts; use hub.createScheduler()
  instead of manually wiring new BackgroundTaskScheduler(reg, drain).
- status.ts: globalBackgroundTaskHub.listByType(EXTRACT_TASK_TYPE, ...)
- Footer.tsx: globalBackgroundTaskHub.registry (shared, filtered by type)
- index.ts: export background/taskHub.js; drop memory/memoryTaskHub.js

* test(background): add BackgroundTaskHub unit tests and hub isolation checks

- background/taskHub.test.ts (11 tests):
  - createScheduler(): tasks registered via scheduler appear in hub registry;
    multiple calls return distinct scheduler instances
  - listByType(): filters by taskType, filters by projectRoot, returns []
    for unknown types, two types co-exist in registry but stay separated
  - drain(): resolves false on timeout, resolves true when tasks complete,
    resolves true immediately when no tasks in flight
  - isolation: tasks in hubA do not appear in hubB
  - globalBackgroundTaskHub: is a BackgroundTaskHub instance with registry/drainer

- extractScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry EXTRACT_TASK_TYPE

- dreamScheduler.test.ts (+1 test):
  - factory-created runtimes have isolated registries; tasks in runtimeA
    are invisible to runtimeB; all tasks carry DREAM_TASK_TYPE

* refactor(memory): consolidate all memory state into MemoryManager

Replace BackgroundTaskRegistry/Drainer/Scheduler/Hub helper classes and
module-level globals with a single MemoryManager class owned by Config.

## Changes

### New
- packages/core/src/memory/manager.ts — MemoryManager with:
  - scheduleExtract / scheduleDream (inline queuing + deduplication logic)
  - recall / forget / selectForgetCandidates / forgetMatches
  - getStatus / drain / appendToUserMemory
  - subscribe(listener) compatible with useSyncExternalStore
  - storeWith() atomic record registration (no double-notify)
  - Distinct skippedReason 'scan_throttled' vs 'min_sessions' for dream
- packages/core/src/utils/forkedAgent.ts — pure cache util (moved from background/)
- packages/core/src/utils/sideQuery.ts — pure util (moved from auxiliary/)

### Deleted
- background/taskRegistry, taskDrainer, taskScheduler, taskHub and all tests
- background/forkedAgent (moved to utils/)
- auxiliary/sideQuery (moved to utils/)
- memory/extractScheduler, dreamScheduler, state and all tests

### Modified
- config/config.ts — Config owns MemoryManager instance; getMemoryManager()
- core/client.ts — all memory ops via config.getMemoryManager()
- core/client.test.ts — mock MemoryManager instead of individual modules
- memory/status.ts — accepts MemoryManager param, drops globalBackgroundTaskHub
- index.ts — memory exports reduced from 14 modules to 5 (manager/types/paths/store/const)
- cli/commands/dreamCommand.ts — via config.getMemoryManager()
- cli/commands/forgetCommand.ts — via config.getMemoryManager()
- cli/components/Footer.tsx — useSyncExternalStore replacing setInterval polling
- cli/components/Footer.test.tsx — add getMemoryManager mock
2026-04-16 20:05:45 +08:00
Reid
07475026f6
fix(cli): remember "Start new chat session" until summary changes (#3308)
* fix(cli): remember "Start new chat session" until summary changes

  Persist a project-scoped Welcome Back restart choice keyed to the
  current PROJECT_SUMMARY fingerprint.

  This suppresses the Welcome Back dialog after choosing "Start new chat
  session", while still showing it again after the project summary is
  updated.

* fix conflict
2026-04-16 13:54:14 +08:00
DennisYu07
b5115e731e
feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* add http/async/function type

* fix url error

* resolve comment

* align cc non blocking error

* fix hookRunner for async

* fix(hooks): update hook type validation to support http and function types

- Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function']
- Add validation for HTTP hooks requiring url field
- Add validation for function hooks requiring callback field
- Add comprehensive test coverage for all hook type validations

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

* fix(hooks): align SSRF protection with Claude Code behavior

- Allow 127.0.0.0/8 (loopback) for local dev hooks
- Allow localhost hostname for local dev hooks
- Allow ::1 (IPv6 loopback) for local dev hooks
- Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598)
- Update tests to match Claude Code's ssrfGuard.ts behavior

This fixes HTTP hooks failing to connect to local dev servers.

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

* refactor(hooks): align HTTP hook security with Claude Code behavior

- Add CRLF/NUL sanitization for env var interpolation (header injection)
- Implement combined abort signal (external signal + timeout)
- Upgrade SSRF protection to DNS-level with ssrfGuard
  - Allow loopback (127.0.0.0/8, ::1) for local dev hooks
  - Block CGNAT (100.64.0.0/10) and IPv6 private ranges
- Increase default HTTP hook timeout to 10 minutes
- Fix VS Code hooks schema to support http type
  - Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields
  - Note: "function" type is SDK-only (callback cannot be serialized to JSON)

* feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support

- Add MessagesProvider for automatic conversation history passing to function hooks
- Add FunctionHookContext with messages, toolUseID, and signal
- Add skillRoot support for skill-scoped session hooks
- Add shell parameter support for command hooks (bash/powershell)
- Add regex matcher support for hook pattern matching
- Add statusMessage to CommandHookConfig
- Change default function hook timeout from 60s to 5s
- Add comprehensive unit tests for all new features

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

* add session hook for skill

* fix function hook parsing

* refactor ui for http hook/async hook/function hook

* update doc and add integration test

* change telemetryn type and refactor SSRF

* fix project level bug

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-16 10:10:33 +08:00
Shaojin Wen
83b394e423
feat(core): implement fork subagent for context sharing (#2936)
* feat(core): implement fork subagent for context sharing

- Make subagent_type optional in AgentTool
- Add forkSubagent.ts to build identical tool result prefixes
- Run fork processes in the background to preserve UX

* fix(core): fix test failures related to root execution and optional subagent_type

- Skip pathReader and edit tool permission tests when running as root
- Fix agent.test.ts to correctly mock execute call with extraHistory
- Remove unused imports in forkSubagent.ts

* fix(core): fix fork subagent bugs and add CacheSafeParams integration

Bug fixes:
- Fix AgentParams.subagent_type type: string -> string? (match schema)
- Fix undefined agentType passed to hook system (fallback to subagentConfig.name)
- Fix hook continuation missing extraHistory parameter
- Fix functionResponse missing id field (match coreToolScheduler pattern)
- Fix consecutive user messages in Gemini API (ensure history ends with model)
- Fix duplicate task_prompt when directive already in extraHistory
- Fix FORK_AGENT.systemPrompt empty string causing createChat to throw
- Fix redundant dynamic import of forkSubagent.js (merge into single import)
- Fix non-fork agent returning empty string on execution failure
- Fix misleading fork child rule referencing non-existent system prompt config
- Fix functionResponse.response key from {result:} to {output:} for consistency

CacheSafeParams integration:
- Retrieve parent's generationConfig via getCacheSafeParams() for cache sharing
- Add generationConfigOverride to CreateChatOptions and AgentHeadless.execute()
- Add toolsOverride to AgentHeadless.execute() for parent tool declarations
- Fork API requests now share byte-identical prefix with parent (DashScope cache hits)
- Graceful degradation when CacheSafeParams unavailable (first turn)

Docs:
- Add Fork Subagent section to sub-agents.md user manual
- Add fork-subagent-design.md design document

* fix(core): apply subagent tool exclusion to forked agents

Fork children were inheriting parent's cached tool declarations directly,
bypassing prepareTools() filtering and gaining access to AgentTool and
cron tools. Extract EXCLUDED_TOOLS_FOR_SUBAGENTS as a shared constant
and apply it to forkToolsOverride.

* fix(core): skip env history whenever extraHistory is provided

Previously gated on generationConfigOverride, which meant the no-cache
fallback path (CacheSafeParams unavailable) still ran getInitialChatHistory
and duplicated env bootstrap messages already present in the parent's
history. Gate on extraHistory instead so both fork paths skip env init.

* fix(core): use explicit skipEnvHistory flag for fork env handling

The previous fix gated env-init skipping on the presence of extraHistory,
but agent-interactive (arena) also passes extraHistory — its chatHistory is
env-stripped by stripStartupContext() and DOES need fresh env init for the
child's working directory. Skipping env there broke the interactive path.

Replace the implicit gate with an explicit skipEnvHistory option that only
fork sets (when extraHistory is present, since fork's history comes from
getHistory(true) and already contains env).

* fix(core): defend skipEnvHistory gate against empty extraHistory

Edge case: when the parent's rawHistory ends with a user message and has
length 1, extraHistory becomes []. The previous gate (extraHistory !==
undefined) would set skipEnvHistory: true, leaving the fork with neither
env bootstrap nor parent history. Check length > 0 so empty arrays fall
through to the normal env-init path.

* fix(core): apply skipEnvHistory to stop-hook retry execute

The second subagent.execute() call in the SubagentStop retry loop was
missing skipEnvHistory, so on retry the fork's env context would be
duplicated — same bug as the initial tanzhenxin report, just on a less
common code path.
2026-04-14 14:27:38 +08:00
tanzhenxin
8d74a0cf0a
feat(subagents): add disallowedTools field to agent definitions (#3064)
* feat(subagents): add disallowedTools field to agent definitions

Add a `disallowedTools` blocklist to agent frontmatter, letting agents
specify tools they should not have access to. Supports exact tool names,
MCP server-level patterns (e.g., `mcp__slack`), and display name aliases.

Applied as a post-filter in AgentCore.prepareTools() after the existing
`tools` allowlist. Persisted through serialize/parse roundtrips.

* docs: document disallowedTools and MCP tool behavior for subagents

Add Tool Configuration section to sub-agents docs explaining:
- tools allowlist and disallowedTools blocklist
- How MCP tools follow the same allowlist/blocklist rules
- MCP server-level patterns in disallowedTools

* fix(subagents): validate disallowedTools in SubagentValidator

Reuse the existing validateTools() method to validate disallowedTools
entries at config validation time, catching non-string and empty entries
before they reach runtime.

* test: remove flaky BaseSelectionList scroll test on Windows
2026-04-13 18:24:02 +08:00
tanzhenxin
0026777828
feat(subagents): propagate approval mode to sub-agents (#3066)
* feat(subagents): propagate approval mode to sub-agents

Replace hardcoded PermissionMode.Default with resolution logic:
- Permissive parent modes (yolo, auto-edit) always win
- Plan-mode parents keep sub-agents in plan mode
- Agent definitions can declare approvalMode in frontmatter
- Default fallback is auto-edit in trusted folders
- Untrusted folders block privileged mode escalation

Also maps Claude permission aliases (acceptEdits, bypassPermissions,
dontAsk) to qwen-code approval modes in the converter.

* fix(subagents): correct dontAsk mapping and add approval mode resolution tests

Map Claude's `dontAsk` to `default` instead of `auto-edit` — `dontAsk`
denies prompts (restrictive) so `default` is a closer semantic match.

Add 9 unit tests covering the full `resolveSubagentApprovalMode` decision
matrix: permissive parent override, agent-declared modes, trusted/untrusted
folder blocking, and plan-mode fallback.

* test: remove flaky InputPrompt tab-suggestion test on Windows
2026-04-13 17:50:26 +08:00
Shaojin Wen
b3bc42931e
feat: add contextual tips system with post-response context awareness (#2904)
* feat: add contextual tips system with post-response context awareness

Add a context-aware tips system that proactively shows helpful tips based
on session state. Post-response tips warn when context usage exceeds 80%
or 95%, suggesting /compress. Startup tips rotate across sessions via LRU
scheduling with cross-session persistence (~/.qwen/tip_history.json).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use value import for runtime values in useContextualTips

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Use lastSessionTimestamp instead of totalShown for cross-session LRU
- Move getTipHistory singleton from Tips.tsx to services/tips/index.ts
- Defer TipHistory.load() when hideTips is true (no side effects)
- Use os.tmpdir() in tests for cross-platform portability
- Add proper translations for de/ja/pt/ru locale files
- Accept TipHistory | null in useContextualTips

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review feedback

- Validate tips field type in TipHistory.load() to handle corrupted JSON
- Split approval-mode tip into platform-specific variants using ctx.platform
- Add afterEach cleanup for temp files in all test suites
- Guard useContextualTips against null tipHistory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: import shared DEFAULT_TOKEN_LIMIT, harden tipHistory, set file permissions

- Import DEFAULT_TOKEN_LIMIT from @qwen-code/qwen-code-core instead of
  hardcoding 1_048_576 in tipRegistry.ts and useContextualTips.ts
- Add normalizeEntry() to defensively handle corrupted tip history entries
- Write tip_history.json with mode 0o600 for privacy on multi-user systems

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove unused compressionThreshold from TipContext

compressionThreshold was defined in TipContext but never used by any tip's
isRelevant check. Remove it to avoid misleading consumers into thinking
tips respect the user's compression settings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sanitize sessionCount and getLastShown against corrupted tip history

- Validate sessionCount is finite and non-negative in TipHistory.load()
- Use normalizeEntry() in getLastShown() for corrupted lastSessionTimestamp

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add contextual tips user documentation

Add docs/users/features/tips.md covering startup tips, post-response
context warnings, tip history persistence, and the hideTips setting.
Update settings.md description and register the new page in _meta.ts.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:40:27 +08:00
DennisYu07
dddb56d885
feat: add stopFailure and postCompact (#2825) 2026-04-13 12:54:44 +08:00
jinye
1557d93043
feat(cli): support tools.sandboxImage in settings (#3146)
Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
2026-04-13 09:43:34 +08:00
pomelo
cf81ac47ff
Merge pull request #3042 from YuchenLiang00/fix/context-command-detail-missing
fix(cli): add 'detail' subcommand to /context command
2026-04-10 12:12:07 +08:00
Shaojin Wen
5482044e59
fix: improve /model --fast description clarity and prevent accidental activation (#3077)
Replace vague "background tasks" with specific "prompt suggestions and speculative
execution" in the --fast flag description across all i18n locales, docs, and VS Code
schema. Update example model name from qwen3.5-flash to qwen3-coder-flash. Also fix
completion logic to require a non-empty partial arg before suggesting --fast, preventing
Tab+Enter from accidentally entering fast model mode.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:09:46 +08:00
Edenman
4d2d4432d5
Merge pull request #2923 from QwenLM/feature/status-line-customization
Some checks are pending
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
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 / 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(ui): add customizable status line with /statusline command
2026-04-09 19:23:08 +08:00
YuchenLiang00
84f80201c7 fix(cli): add 'detail' subcommand to /context command
The /context command was missing the subcommand autocomplete feature
that other commands like /stats have. Now users can type '/context '
and see 'detail' as a suggestion in the dropdown.

- Added 'detail' subCommand to contextCommand with its own description
- Subcommand delegates to main action with 'detail' arg
- Added missing translation key for full description in zh.js
- Updated commands.md documentation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 11:00:04 +08:00
wenshao
6de5c9e530 Merge remote-tracking branch 'origin/main' into feat/review-skill-improvements 2026-04-09 08:52:01 +08:00
Shaojin Wen
f208801b0e
fix(followup): prevent tool call UI leak and Enter accept buffer race (#2872)
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
* fix(core): prevent followup suggestion input/output from appearing in tool call UI

The follow-up suggestion generation was leaking into the conversation UI
through three channels:

1. The forked query included tools in its generation config, allowing the
   model to produce function calls during suggestion generation. Fixed by
   setting `tools: []` in runForkedQuery's per-request config (kept in
   createForkedChat for speculation which needs tools).

2. logApiResponse and logApiError recorded suggestion API events to the
   chatRecordingService, causing them to appear in session JSONL files
   and the WebUI. Fixed by adding isInternalPromptId() guard that skips
   chatRecordingService for 'prompt_suggestion' and 'forked_query' IDs.
   uiTelemetryService.addEvent() is preserved so /stats still tracks
   suggestion token usage.

3. LoggingContentGenerator logged suggestion requests/responses to the
   OpenAI logger and telemetry pipeline. Fixed by skipping logApiRequest,
   buildOpenAIRequestForLogging, and logOpenAIInteraction for internal
   prompt IDs. _logApiResponse is preserved (for /stats) but its
   chatRecordingService path is filtered by fix #2.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: deduplicate isInternalPromptId into shared export from loggers.ts

Address review feedback: extract isInternalPromptId() to a single
exported function in telemetry/loggers.ts and import it in
LoggingContentGenerator, eliminating the duplicate private method.

Also update loggingContentGenerator.test.ts mock to use importOriginal
so the real isInternalPromptId is available during tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract isInternalPromptId to shared utils, add tests

Address maintainer review feedback:

1. Move isInternalPromptId() to packages/core/src/utils/internalPromptIds.ts
   using a ReadonlySet for the ID registry. Adding new internal prompt IDs
   only requires changing one file. loggers.ts re-exports for compatibility,
   loggingContentGenerator.ts imports directly from utils.

2. Extract `tools: []` magic value to a frozen NO_TOOLS constant in
   forkedQuery.ts.

3. Add unit tests for isInternalPromptId: prompt_suggestion → true,
   forked_query → true, user_query → false, empty string → false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review — docs, stream optimization, tests

1. Update forkedQuery.ts module docs to reflect that runForkedQuery
   overrides tools: [] at the per-request level while createForkedChat
   retains the full generationConfig for speculation callers.

2. Propagate isInternal into loggingStreamWrapper to skip response
   collection and consolidation for internal prompts, avoiding
   unnecessary CPU/memory overhead.

3. Add logApiResponse chatRecordingService filter tests: verify
   prompt_suggestion/forked_query skip recording while normal IDs
   still record.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: deep-freeze NO_TOOLS, add internal prompt guard tests

Address Copilot review round 3:

1. Deep-freeze NO_TOOLS.tools array to prevent shared mutable state
   across forked query calls.

2. Add LoggingContentGenerator tests verifying that internal prompt IDs
   (prompt_suggestion, forked_query) skip logApiRequest and OpenAI
   interaction logging while preserving logApiResponse.

3. Add logApiError chatRecordingService filter tests matching the
   existing logApiResponse coverage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: reconcile createForkedChat JSDoc with module header

Clarify that createForkedChat retains the full generationConfig
(including tools) for speculation callers, while runForkedQuery
strips tools at the per-request level via NO_TOOLS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: build errors and Copilot round 4 feedback

1. Fix NO_TOOLS type: Object.freeze produces readonly array incompatible
   with ToolUnion[]. Use Readonly<Pick<>> instead; spread in requestConfig
   already creates a fresh mutable copy per call.

2. Fix test missing required 'model' field in ContentGeneratorConfig.

3. Track firstResponseId/firstModelVersion in loggingStreamWrapper so
   _logApiResponse/_logApiError have accurate values even when full
   response collection is skipped for internal prompts.

4. Strengthen OpenAI logger test assertion: assert OpenAILogger was
   constructed (not guarded by if), then assert logInteraction was
   not called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove dead Object.keys check, add streaming internal prompt test

1. Simplify runForkedQuery: requestConfig always has tools:[] from
   NO_TOOLS spread, so the Object.keys().length > 0 ternary is dead
   code. Pass requestConfig directly.

2. Add generateContentStream test for internal prompt IDs to match
   the existing generateContent coverage, ensuring the streaming
   wrapper also skips logApiRequest and OpenAI interaction logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent Enter accept from re-inserting suggestion into buffer

When accepting a followup suggestion via Enter, accept() queued
buffer.insert(suggestion) in a microtask that executed after
handleSubmitAndClear had already cleared the buffer, leaving the
suggestion text stuck in the input.

Add skipOnAccept option to accept() so the Enter path bypasses the
onAccept callback. Also add runForkedQuery unit tests verifying
tools: [] is passed in per-request config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): add speculation to internal IDs, fix logToolCall filtering, improve suggestion prompt

- Add 'speculation' to INTERNAL_PROMPT_IDS so speculation API traffic
  and tool calls are hidden from chat recordings and tool call UI
- Add isInternalPromptId check to logToolCall() for consistency with
  logApiError/logApiResponse
- Improve SUGGESTION_PROMPT: prioritize assistant's last few lines and
  extract actionable text from explicit tips (e.g. "Tip: type X")
- Fix garbled unicode in prompt text
- Update design docs and user docs to reflect changes
- Add test coverage for all new behavior

* fix(core): deep-freeze NO_TOOLS, add speculation to loggingContentGenerator tests

- Object.freeze NO_TOOLS and its tools array to prevent runtime mutation
- Add 'speculation' to loggingContentGenerator internal prompt ID tests
  for consistency with loggers.test.ts and internalPromptIds.ts

* fix(core): fix NO_TOOLS Object.freeze type error

Use `as const` with type assertion to satisfy TypeScript while keeping
runtime immutability via Object.freeze.

* refactor(core): remove unused isInternalPromptId re-export from loggers.ts

All consumers import directly from utils/internalPromptIds.js.
The re-export was dead code with no importers.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 00:07:03 +08:00
wenshao
3364cf880f Merge remote-tracking branch 'origin/main' into feat/review-skill-improvements 2026-04-08 23:09:01 +08:00
wenshao
bcd0b5efe6 docs: update status line documentation to reflect inline footer layout
- Update architecture diagram to show status line in footer left
  section instead of separate row below
- Document 1-row (default mode) and 2-row (non-default mode) layouts
- Note suppressHint behavior and truncation
- Update settings reference description
2026-04-08 21:07:07 +08:00
wenshao
a1c33cdb5e refactor(status-line): remove padding config
The status line is now inlined in the footer's left section,
so horizontal padding is no longer applicable. Remove padding
from StatusLineConfig, settings schema, JSON schema, and docs.
2026-04-08 20:24:33 +08:00
wenshao
0be4d32cb0 Merge remote-tracking branch 'origin/main' into feature/status-line-customization 2026-04-08 18:50:10 +08:00
克竟
24a28d5fb0 refactor(status-line): redesign JSON input schema and add context fields
Restructure the status line stdin JSON for clarity and accuracy:
- Rename model.id → model.display_name, cwd → workspace.current_dir
- Replace raw context_window size/count with used_percentage,
  remaining_percentage, current_usage, context_window_size, and
  total_input_tokens/total_output_tokens
- Add version field from cfg.getCliVersion()
- Add git.branch, metrics.models, metrics.files
- Remove upstream-only fields: tokens.tool (never populated),
  session (start_time/elapsed_time not live-updating),
  streaming_state, approval_mode, terminal, metrics.tools
- Rename tokens.candidates → tokens.completion (Qwen API convention)
- Fix template string escaping in builtin-agents to avoid
  templateString() placeholder collision

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:52:07 +08:00
wenshao
1d4c17b1ce fix: rename /plan execute to /plan exit
Rename the subcommand to accurately reflect its behavior (exits plan
mode and restores previous approval mode, does not trigger execution).
Update source, tests, i18n keys (6 locales), and docs.
2026-04-08 10:21:00 +08:00
wenshao
820191d99d docs: add zero-findings PR tip to follow-up table
User doc and PR description now include the "PR review, zero findings
→ post comments → approve PR" row in the follow-up actions table.
Also fixed PR description: "Step 4" → "Step 9" for post comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:33:03 +08:00
wenshao
a9038a1769 fix(review): 5 issues — CI security, incremental+comment, doc accuracy
1. CI config auto-discovery: read from base branch for PR reviews
   (PR branch is untrusted, malicious PR could inject commands)
2. Incremental early-exit: don't block --comment on unchanged PR —
   allow posting comments from previous review findings
3. Doc: review summary not always posted (Comment verdict skips it)
4. Doc: cross-repo reviews skip report persistence
5. Doc: clarify "Agents 1-4 findings verified" (not all — reverse
   audit findings skip verification)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 14:40:16 +08:00
wenshao
c826c24f6e fix(review): skip Agent 5 in cross-repo mode, update token counts
Cross-repo lightweight mode has no local codebase — Agent 5 (build/test)
is pointless. Now launches 4 agents instead of 5 in cross-repo mode.

Updated token count tables in SKILL.md, user doc, and DESIGN.md:
same-repo = 7 LLM calls, cross-repo = 6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:24:59 +08:00
wenshao
8f723575bd docs: add CI config auto-discovery to user doc and design doc
- User doc: added "Other" row to language table + explanation that
  CI config is read for unrecognized projects
- DESIGN.md: added "Why auto-discover from CI config" decision
  section + added .qwen/review-tools.md to rejected alternatives

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:47:07 +08:00
wenshao
cbf2aa06ac feat(review): add Java and C/C++ support for deterministic analysis
Step 3 now supports:
- Java: mvn compile, checkstyle, spotbugs, pmd (Maven);
  gradle compileJava, checkstyleMain (Gradle)
- C/C++: clang-tidy (when compile_commands.json available)

Agent 5 build/test precedence now includes Maven and Gradle
before Makefile to avoid duplicate builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:19:56 +08:00
wenshao
720796e699 fix(review): safe branch cleanup + cross-repo agent count
- git branch -D: add 2>/dev/null || true to both cleanup sites
  (Step 1 stale cleanup + Step 11) to prevent abort if ref missing
- Cross-repo doc: clarify Agents 1-4 only (Agent 5 build/test
  requires local codebase, not available in cross-repo mode)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 05:02:13 +08:00
wenshao
559f2efd42 fix(review): fix cross-repo mode and add documentation
SKILL.md:
- Step 9 must use owner/repo from URL (not gh repo view) for cross-repo
- Step 2 (project rules) skipped in cross-repo mode (no local files)

User doc: add Cross-repo PR Review section with same-repo vs cross-repo
capability comparison table.

DESIGN.md: add "Why cross-repo uses lightweight mode" section explaining
CLI tools are inherently repo-local and our approach is best available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 04:18:55 +08:00
wenshao
01c105b5e7 refactor(review): renumber steps from sub-steps to sequential 1-11
Replace confusing sub-step numbering (1, 1.1, 1.5, 2, 2.5, 2.6, 3,
3.5, 4, 4.5, 5) with clean sequential numbering (1-11).

Mapping: 1→1, 1.1→2, 1.5→3, 2→4, 2.5→5, 2.6→6, 3→7, 3.5→8,
4→9, 4.5→10, 5→11

Updated all cross-references in SKILL.md, user docs, and PR description.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 04:12:45 +08:00
wenshao
09c95c44c8 fix(review): address 5 Copilot comments
- Add error handling for git fetch and gh pr view failures in Step 1
- Skip worktree cleanup on autofix commit/push failure (preserve
  uncommitted fixes for manual recovery)
- Fix Agent 5 counting: it's 1 of the 5 LLM agents (not a separate
  zero-cost stage). Remove misleading "zero LLM" annotation and
  duplicate row from token efficiency table.
- Reverse audit skip-verification already implemented (comment #53
  was stale)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:42:20 +08:00
wenshao
6d7afafe3c perf(review): skip verification for reverse audit findings
Reverse audit agent already has full context (all confirmed findings +
entire diff), so its findings don't need a second opinion. This brings
the actual LLM call count to 7 (5 review + 1 verify + 1 reverse),
matching the documented claim.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:38:49 +08:00
wenshao
d6b9b35350 fix(review): handle interrupted review cleanup
If a previous review was interrupted (Ctrl+C, crash), stale worktree
and local ref would block the next review. Now Step 1 checks for and
cleans up stale .qwen/tmp/review-pr-<N> worktree and qwen-review/pr-<N>
ref before creating new ones.

Step 5 also cleans up the local ref alongside the worktree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 03:35:00 +08:00