Commit graph

2712 commits

Author SHA1 Message Date
wenshao
cc916b528e docs(attribution): correct legacyTypes / EXCLUDED_DIRECTORY_SEGMENTS comments
9Ta_ (Copilot): the JSDoc on legacyTypes claimed JSON Schema's
`type` keyword does not accept `'object'` — that's wrong; `'object'`
IS a valid JSON Schema type. Reword to reflect the actual rationale:
`'enum'` is not a valid JSON Schema `type` value at all (enum
constraints use the `enum` keyword), and a bare `{type: 'object'}`
would accept any object regardless of what the field's pre-expansion
shape actually allowed. The narrowed `boolean | string | number |
array` set is exactly what the one-liner generator can faithfully
emit; richer legacy shapes belong in their own branch of
convertSettingToJsonSchema.

9Tbs (Copilot): the comment in generatedFiles.ts referenced
`EXCLUDED_DIRECTORIES`, but the constant is `EXCLUDED_DIRECTORY_SEGMENTS`
(renamed during the segment-boundary refactor). Update the
reference so a future maintainer scanning for the rule doesn't
chase a non-existent identifier.
2026-05-06 20:47:43 +08:00
wenshao
a66a21d81d fix(attribution): GIT_DIR repo-shift bail, snapshot envelope validation, narrow legacyTypes
80ME (gpt-5.5 /review, [Critical]): tokeniseSegment unconditionally
stripped every leading KEY=value token. `GIT_DIR=elsewhere/.git git
commit ...` was therefore treated as an in-cwd commit, picked up the
Co-authored-by trailer, and produced a per-file note that landed
against our cwd's HEAD even though the actual commit went to a
different repo. Define a GIT_ENV_SHIFTS_REPO set (GIT_DIR,
GIT_WORK_TREE, GIT_COMMON_DIR, GIT_INDEX_FILE, GIT_NAMESPACE) and
have tokeniseSegment refuse to parse any segment whose leading env
block (including the env-wrapper's KEY=VALUE block) carries one of
these. Identity / date variables (GIT_AUTHOR_*, GIT_COMMITTER_*) are
deliberately NOT in the set — they tweak metadata but don't relocate
the repo. Tests cover plain prefix, env-wrapped prefix, and a
GIT_COMMITTER_DATE positive control that should still get the trailer.

8EeQ (Copilot): restoreFromSnapshot received `snapshot as
AttributionSnapshot` from a structural cast off `unknown` (the
resume path), so its TS-typed shape was only a hint. A corrupted
JSONL line (non-object / array / wrong type discriminator / missing
type) would skip past the version check straight into
Object.entries(snapshot.fileStates) — and a non-object fileStates
(an array, say) seeded fileAttributions with numeric-string keys.
Add envelope-level shape gates (isPlainObject + type discriminator)
and a fileStates plain-object check before iterating; both bail to a
clean reset rather than poisoning the singleton. Tests added.

8Eej (Copilot): SettingDefinition.legacyTypes was typed as
SettingsType[] which includes 'enum' and 'object' — JSON Schema's
`type` keyword doesn't accept those values. Adding
`legacyTypes: ['enum']` would silently produce an invalid
settings.schema.json. Narrow the field's type to
ReadonlyArray<'boolean' | 'string' | 'number' | 'array'> (the
JSON-Schema-primitive subset). Future complex-shape legacy support
should land its own branch in convertSettingToJsonSchema.
2026-05-06 20:34:41 +08:00
wenshao
acd06e3461 Merge remote-tracking branch 'origin/main' into feat/commit-attribution
# Conflicts:
#	packages/core/src/core/client.ts
2026-05-06 15:02:52 +08:00
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
易良
fe1fb31557
fix(cli): prevent file paths from being treated as slash commands (#3743)
* fix(cli): prevent file paths from being treated as slash commands (#1804)

When users input file paths starting with '/' (e.g. '/api/apiFunction/...',
'/Users/name/path'), they were incorrectly parsed as slash commands, resulting
in "Unknown command" errors. The input was discarded instead of being sent to
the model for processing.

Root cause: isSlashCommand() only checked for a '/' prefix without validating
whether the first token actually looks like a command name. Any '/' prefix
triggered the slash command flow, and when no matching command was found, the
error was shown with no fallback.

Fix: Add looksLikeCommandName() that validates command names contain only
[a-zA-Z0-9:_-]. Both isSlashCommand() and handleSlashCommand() now check the
first token — if it contains path separators, dots, or non-ASCII characters,
the input falls through to normal model processing instead of the command
dispatcher.

Closes #1804

* fix(cli): allow dots in command names and fix prettier formatting

Address review feedback:
- Allow '.' in looksLikeCommandName() regex to support extension-qualified
  commands like gcp.deploy (CommandService renames conflicts as ext.cmd)
- Add regression tests for dot-named commands in both commandUtils and
  slashCommandProcessor
- Fix prettier formatting in slashCommandProcessor test file

* fix(cli): handle slash command review edge cases

* docs(cli): align slash command validation comment

* fix(cli): preserve slash prompt ordering

* fix(cli): reject shell-metacharacter slash tokens

* test(cli): align slash command action mocks

* fix(cli): track model-sent user turns

* fix(cli): narrow slash path handling scope

* fix(cli): count slash path prompts in history

* test(cli): type slash command action mocks
2026-05-06 14:03:08 +08:00
Shaojin Wen
d44786685c
feat(core,cli): surface and cancel auto-memory dream tasks (#3836)
* feat(cli): surface auto-memory dream tasks in Background tasks dialog

Adds `dream` as a fourth kind in the Background tasks pill + dialog,
alongside agent / shell / monitor. Subscribes to MemoryManager via
its existing subscribe() / listTasksByType() API and adapts
MemoryTaskRecord into a DreamDialogEntry view-model. Zero changes
to the core package.

Filters out `pending` (sub-second transition) and `skipped` (every
UserQuery that misses the gate creates one) records. Caps retained
terminal entries at 3 since `MemoryManager.tasks` has no eviction
path; without the cap, completed dreams would accumulate over the
project's lifetime (mirrors MonitorRegistry's terminal cap pattern).

Extract tasks are intentionally NOT surfaced — they fire on every
UserQuery, would flood the pill, and the `memory_saved` toast in
useGeminiStream already covers their completion signal.

Read-only for now: cancellation requires MemoryManager.cancelTask
+ task_stop integration which lands in a follow-up PR. The dialog
suppresses the "x stop" hint for dream entries until then to avoid
silent no-op keystrokes.

Refs #3634

* docs(cli): rephrase dream filter comment to focus on extract vs dream

The earlier comment compared the design to a non-qwen-code product;
restate the rationale in terms of the local extract / dream split
(extract fires every UserQuery and surfaces via memory_saved toast,
dream fires after gates and warrants pill / dialog visibility).

* feat(core,cli): cancel dream consolidation tasks via dialog and task_stop

Wires cancellation for the auto-memory dream task kind:

- `MemoryManager.cancelTask(taskId)` — aborts the dream's fork-agent
  via a new per-task AbortController, marks the record `cancelled`
  before aborting so the runDream catch path can detect user-intent
  and avoid overwriting with a generic `failed`. The existing
  finally block releases the consolidation lock as the agent unwinds.
- `MemoryManager.getTask(id)` — point lookup helper so cross-cutting
  consumers like `task_stop` can route by id without a project root.
- AbortSignal threaded through `scheduleDream` → `runDream` →
  `runManagedAutoMemoryDream` → `planManagedAutoMemoryDreamByAgent`
  → `runForkedAgent.abortSignal` (already supported).
- `task_stop` tool gets a 4th dispatch branch: dream task ids look
  up via MemoryManager and route through `cancelTask`. Extract is
  intentionally NOT cancellable — it runs synchronously on the
  request loop, cancelling would interfere with the user's own turn.
- `BackgroundTasksDialog` x-stop hint suppression for dream is removed
  (was a PR-1 placeholder); `cancelSelected` dream branch now calls
  `memoryManager.cancelTask`.
- `MemoryTaskStatus` gains `'cancelled'`. Dream view-model widens
  status union and filter to surface cancelled entries (terminal
  cap continues to apply).

Refs #3634

* fix(core,cli): address review feedback on dream cancellation surface

- DreamDetailBody: comment said cancellation "lands in PR-2" but the
  same PR wires `cancelSelected` for dream entries. Reword to describe
  what's actually shipped + flag in-flight progress as the real
  follow-up.
- task_stop: drop the unreachable `!aborted` error branch. The status
  guard above already confirms `running`, and `cancelTask` is
  synchronous; in this branch it cannot return false.

* fix(core): handle resolved-cancel path from runForkedAgent for dream tasks

runForkedAgent maps AgentTerminateMode.CANCELLED to a resolved
{status: 'cancelled'} result rather than rejecting. The cancel-via-
task_stop path landed in the previous commit assumed the call would
throw — when it didn't, the runDream success path overwrote the
user-cancelled record with 'completed' AND bumped lastDreamAt
metadata, suppressing the next legitimate dream cycle.

Two-layer defense:

- dreamAgentPlanner now rethrows when the fork agent reports
  cancelled status (mirrors the existing failed-status throw).
  This is the source-of-truth fix.

- runDream now checks abortSignal.aborted after the await as
  defense in depth. If anything in the call chain ever forgets to
  propagate, this guard short-circuits the success path before
  metadata write.

Updates the existing dreamAgentPlanner test that previously pinned
the buggy "returns cancelled without throwing" behavior. Adds a
manager test that simulates the resolves-on-abort scenario directly
to verify the consumer-side guard catches anything the planner
might miss.

* fix(core,cli): address review feedback on dream UX, perf, and comment clarity

Three fixes from the post-cancellation-PR review:

- scheduleDream now sets an initial `progressText` ("Scheduled
  managed auto-memory dream.") on the in-flight record. Without
  this, the dialog Detail's Progress section stayed empty until
  completion — the PR description's mid-flight screenshot showed
  text that production never actually rendered.

- useBackgroundTaskView gates the MemoryManager.subscribe listener
  on a dream-content signature. The manager fires for every task
  transition (extract included, ~2x per UserQuery), but the dialog
  has no extract surface; without this dedup each extract notify
  forced a full 4-source re-merge + a fresh setEntries reference,
  re-rendering the dialog and pill on entries that hadn't changed.
  Added a test that pins the reference-stability invariant.

- task-stop comment was misleading — said "the status guard above
  already confirmed running", but for the dream branch the running
  check happens IN this branch (not earlier). Reworded.

* fix(core,cli): tighten cancelTask contract, dedup dream snapshot reads, sync test helper

Four fixes from the latest review pass:

- cancelTask() now enforces the AbortController invariant. The old
  `ac?.abort()` returned `true` even when no controller was found,
  meaning callers could see a successful return while the dream was
  not actually aborted (and would leak the consolidation lock until
  the agent finished naturally). The controller is registered
  synchronously alongside `status='running'`, so a missing controller
  for a running record is a contract violation — return false without
  flipping status so the caller knows the abort didn't take.

- useBackgroundTaskView's `refresh()` now reuses the dream snapshot
  the memory listener fetched for its dedup gate. The previous version
  re-read `listTasksByType('dream')` inside `computeDreamSig()` and
  again inside `refresh()` — extra work AND a race window where the
  gate signature could come from a different snapshot than the one
  used to build dreamEntries. Single read, single source of truth.

- The `dream()` test helper widened to include `'cancelled'` so it
  matches the production `MemoryTaskStatus` union. Added a small test
  asserting cancelled dreams flow through the kind discriminator (the
  dialog's terminal-cap window depends on showing the user the
  outcome of the abort they just triggered).

- Dropped the stale "PR-1 read-only / PR-2 cancellation" block
  comment above the dream-entries describe block — both are in this
  PR now.

* fix(core,cli): address self-review + Copilot feedback on dream surface

Combined fixes for the latest review pass — self-review notes (U1, U2,
U5, U6) plus Copilot's three new comments (C1, C2, C3):

- **Footer dream-indicator dedupe (U1)**: removed `useDreamRunning` +
  `✦ dreaming` right-column text. The new Background tasks pill
  already counts dream tasks alongside agent / shell / monitor;
  showing both produced two simultaneous signals for the same state.

- **task_stop dispatch surfaces extract distinctly (U2 + C1)**: a
  new `TASK_STOP_NOT_CANCELLABLE` error type fires when the task id
  resolves to a known-but-not-cancellable record (extract). Previously
  extract ids fell through to `NOT_FOUND`, misleading the model into
  thinking the id was never valid. Also surfaces the missing-controller
  case from `cancelTask` as an explicit error rather than reporting
  phantom success.

- **MemoryManager.cancelTask logs missing-controller violation (C2)**:
  the silent `return false` for the missing-AbortController case now
  emits a `debugLogger.warn` so the inconsistency is observable in
  debug bundles. Without the log a runaway dream burning tokens would
  leave no trail.

- **MemoryManager.subscribe taskType filter (C3)**: subscribers can
  now opt into per-type notify routing via `subscribe(fn, { taskType })`.
  Internal `notify()` calls pass the changed task's type so filtered
  consumers wake only on relevant transitions. The bg-tasks UI hook
  uses this to skip the per-UserQuery extract notify entirely — drops
  the per-extract O(n) signature work to zero.

- **runDream guards against late-cancel overwrite (U5)**: the
  success path now re-checks `abortSignal.aborted` between metadata
  read/write and before the final `update({status: 'completed'})`.
  Closes the ~tens-of-ms race window where `cancelTask` flipping
  status to `'cancelled'` would silently lose to the success
  continuation overwriting with `'completed'` + bumping
  `lastDreamAt`.

- **DreamDialogEntry.endTime semantic comment (U6)**: documents that
  `endTime` for cancelled records is the cancel-call moment (not the
  fork unwind), so a future maintainer doesn't treat it as a real
  fork-finish timestamp.

Tests: new `subscribe() taskType filter` describe block in manager,
new task_stop tests for `NOT_CANCELLABLE` (extract) and missing-
AbortController (dream); existing test renamed/widened.

* fix(core,cli): tighten error semantics + comments on dream surface

Four fixes from the latest review pass:

- New `TASK_STOP_INTERNAL_ERROR` error type for the missing-
  AbortController contract violation. Previously the dispatcher
  reused `TASK_STOP_NOT_RUNNING`, which is misleading — the task IS
  running, cancellation just couldn't be delivered. Distinct type
  signals "this is unexpected, file a bug" vs `NOT_CANCELLABLE`
  which signals "expected behavior, use a different approach".

- Reworded the `useBackgroundTaskView` filter comment. Said "every
  UserQuery that misses the gate creates one [skipped record]" but
  `scheduleDream` returns `{status: 'skipped'}` early without
  creating a record for most gate misses; only the
  acquireDreamLock/EEXIST race actually stores a `'skipped'` record.

- Strengthened the `subscribe() unsubscribe` test. The previous
  version asserted "not called yet" without firing any notify after
  unsubscribe, so a regression that left the listener attached
  would still pass. Now schedules an extract before AND after the
  unsubscribe, verifying the call count doesn't increment.

- Moved `const debugLogger = createDebugLogger(...)` below the
  full import block in manager.ts. Previous version sat between
  imports, violating eslint-plugin-import's `import/first` rule
  (didn't trip lint locally, but worth fixing before it does).

* fix(core): skip scheduleDream early when params.config is missing

`ScheduleDreamParams.config` is optional in the type so test paths
can omit it, but production callers always pass one. Without a
config, `runManagedAutoMemoryDream` throws because the fork-agent
execution requires it. With dream tasks now visible in the
Background tasks dialog, that throw becomes a noisy `failed` entry
the user sees but didn't trigger.

Convert the omitted-config case to the same `disabled` skip path
that an explicitly-disabled config takes, so a no-config call
short-circuits before any record is stored. Existing tests that
relied on the old "no config = proceed past the disabled gate"
behavior now pass an explicit `makeMockConfig()` (matching what
they would do in any realistic scenario).

New test pins the no-config skip behavior + asserts no record
was stored (so a regression that drops the early skip would
produce a visible failed entry in the dialog and fail the test).

* fix(core): plug subscribe Map leak + dream.ts late-cancel ordering bug

Two fixes from the latest review pass:

- `MemoryManager.subscribe`'s typed-branch unsubscribe deleted the
  listener from its per-type Set but left the empty Set sitting in
  `subscribersByType`. Over a long-running session with repeated
  React mount/unmount of the bg-tasks view, that accumulates dead
  Map entries forever. Drop the entry when the bucket goes empty.

- `runManagedAutoMemoryDream` writes metadata after the fork agent
  returns (`bumpMetadata` → `rebuildManagedAutoMemoryIndex` →
  `updateDreamMetadataResult`). If the user presses 'x' between the
  fork's success return and these writes, the writes proceed and
  bump `lastDreamAt` — leaving the visible UI ('Stopped') disagreeing
  with the scheduler gate (sees a recent successful dream and
  suppresses the next cycle). manager.ts already short-circuits its
  own metadata write via the post-await abort check, but it can't
  block writes that already happened inside dream.ts. Adds the same
  abort-signal check between each write step here.

* fix(core): swallow releaseDreamLock errors so they don't poison outcome

If `releaseDreamLock` throws inside the inner finally (e.g. filesystem
error on the lock file), the exception propagates to the outer catch
and overwrites a successfully-completed dream record with 'failed'.
The on-disk metadata is already up-to-date at that point, so the user
sees a contradictory state — `lastDreamAt` was bumped but the UI
shows a failure.

Wrap the release in a try/catch with `debugLogger.warn`. The lock
file is still cleaned up on the next session via the existing
staleness sweep, so swallowing the release error doesn't risk a
permanent stuck lock.

* fix(core): thread abortSignal into dream metadata writes

The pre-call abort checks in `runManagedAutoMemoryDream` close most
of the late-cancel race window, but each metadata helper itself does
read → mutate → write across two awaits. If the user cancels between
those two awaits the write still happens, persisting `lastDreamAt`
for an aborted run and suppressing the next legitimate dream cycle.

Thread `abortSignal` into `bumpMetadata` and
`updateDreamMetadataResult`; both now re-check between the read and
the write, returning early without persisting when the signal has
already fired. The pre-call checks remain as the first line of
defense; this guards the race that opens after the call enters.

* fix(core): close dream cancel race + surface lock-release failures

Two related fixes from the latest review pass:

- Move scheduler-gating metadata writes out of `runManagedAutoMemoryDream`
  and into `MemoryManager.runDream`, sequenced AFTER the
  status='completed' flip. The previous shape left a race window
  where cancellation arriving during/after `fs.writeFile` could
  persist `lastDreamAt` while the manager flipped status to
  'cancelled' — visible UI ('Stopped') would disagree with the
  scheduler gate (sees a recent successful dream), suppressing the
  next legitimate dream cycle. The new order makes the gating
  metadata write race-free: once status !== 'running', cancelTask
  refuses, so any cancel arriving during the metadata write is
  ignored. The remaining "cancel raced the synchronous status
  update" window is handled by a post-update abort recheck that
  restores 'cancelled' and skips the metadata write.

  Drops the now-dead `bumpMetadata` helper from dream.ts; index
  rebuild stays there since it's informational, not gating.

- Surface `releaseDreamLock` failures on the task record's metadata
  (`lockReleaseError`). The previous fix logged-and-swallowed only,
  so a Windows EPERM or ENOENT race would silently block subsequent
  dreams as 'locked' with no UI signal explaining why dreaming had
  stopped. Logger keeps emitting the warn for debug bundles.

* fix(core,cli): address 8 review findings on dream surface

Reverts the metadata-write behavior regression, plugs the
storeWith reentrancy hole, surfaces lock/metadata warnings in the
UI, and a handful of cleanups:

- Wrap gating-metadata read+write in try/catch (manager.ts). The
  PR moved metadata writes from dream.ts (best-effort, swallowed)
  to manager.ts (unguarded). A throw from readDreamMetadata /
  writeDreamMetadata now propagates to the outer catch and
  overwrites a successfully-completed dream with 'failed' — the
  dream actually did its work and touched files are visible. New
  catch logs + writes `metadataWriteError` on the record so the UI
  can explain why the next dream may re-fire sooner than expected.

- Register the AbortController BEFORE storeWith in scheduleDream.
  storeWith fires a notify; a subscriber synchronously calling
  cancelTask(record.id) would otherwise see status='running' but
  no controller, hitting the missing-controller defensive warn
  path and reporting a phantom failure on a brand-new dream.

- Surface `lockReleaseError` and `metadataWriteError` in the dream
  view-model and DreamDetailBody (rendered as warnings, not
  errors, so the terminal status stays Completed). Previous fix
  wrote them to record.metadata only — nothing in the cli read or
  rendered them, so users still had no UI signal.

- Preserve `result.touchedTopics` on the unreachable cancel-raced-
  status-update branch. If a future refactor introduces an await
  there, the restored cancelled record would otherwise drop the
  already-produced result; the UI would report a clean cancellation
  even though memory files were already modified.

- Add `'cancelled'` to MemoryDreamEvent status union and emit a
  cancelled telemetry event from the runDream catch path. Without
  this a cancelled dream is indistinguishable from one that never
  scheduled in the first place.

- Drop the dead abortSignal param from updateDreamMetadataResult
  in dream.ts (no caller passes it after the manager.ts move).

- Swap declaration order of `computeDreamSig` and `refresh` in
  useBackgroundTaskView.ts (TDZ-fragile against a future refactor
  that adds a synchronous refresh call between them).

- Update the unreachable cancel-raced-update branch comment to
  describe what's actually true ("defense-in-depth, unreachable
  today") instead of the confusing "cancelTask flipped" path.

- cancelSelected now checks the cancelTask return value and logs
  via debugLogger when false. Today this branch is unreachable
  thanks to the controller-register-before-storeWith fix above,
  but if a future refactor breaks the invariant the silent ignore
  would let the user think the cancel took effect.

- Mirror the manual /dream metadata path's `recentSessionIdsSinceDream = []`
  reset in the auto path — field is dead code today but keeping
  the two write sites in sync avoids surprises.

Telemetry metric `recordMemoryDreamMetrics` widened to accept the
new 'cancelled' status (downstream consumer in loggers.ts).

* fix(memory): same-session recovery when releaseDreamLock throws

The prior fix surfaced lockReleaseError on the dialog so the user knows
the lock release failed (Windows EPERM, ENOENT race, disk full, etc.) —
but until next process start, dreamLockExists() still sees a fresh-mtime
lock owned by an alive PID (us!) and silently suppresses every
subsequent scheduleDream() call as `{status: 'skipped',
skippedReason: 'locked'}`. The user sees the warning AND zero further
dream activity, and the staleness sweep that would clean the leaked
lock only runs at session start.

Adds a `dreamLockReleaseFailed` flag set in the catch. The next
scheduleDream() force-cleans the leaked lock file via fs.rm({force: true})
before the existence check, so dream scheduling resumes within the same
session. Best-effort: if even the forced rm fails (truly unrecoverable
filesystem state), falls through to the existing 'locked' skip path.

This is an incremental improvement on top of b00ecdebd's UI-surface fix.
The two together give the full story: warning visible → automatic
recovery on next attempt.

* fix(memory): real cancelled-dream duration + clarify index-rebuild ordering

Two follow-up suggestions from review:

- **`duration_ms: 0` in cancelled-dream telemetry** (manager.ts):
  the user-cancel path emitted `MemoryDreamEvent` with
  `duration_ms: 0`, which would silently skew latency histograms / p95
  metrics by treating cancelled dreams as instant. Capture
  `dreamStartMs = Date.now()` at the top of `runDream` and emit the
  real elapsed time in the cancel branch.

- **Misleading index-rebuild comment** (dream.ts:75–84): the comment
  claimed the index rebuild "is still done before returning when
  topics were touched", but the code returns early on
  `abortSignal?.aborted` BEFORE the rebuild. Rewrote the comment to
  describe the actual cancel-aware ordering — abort returns partial
  result without rebuilding (rebuild is expensive; next dream cycle
  will rebuild against the latest files anyway), live path rebuilds
  only when topics changed.

22 / 22 manager tests pass; tsc clean.
2026-05-06 10:20:19 +08:00
wenshao
d429d90331 fix(attribution): toggle-off partial clear, normalizeGitCoAuthor type-check, terraform lockfile
0oAK (Copilot): the gitCoAuthor.commit toggle-off branch returned
before computing the committed file set, leaving the just-committed
files' tracked AI work in the singleton. Re-enabling the toggle and
committing the same file again would re-attribute earlier (already-
committed) AI edits to the new commit. Move the toggle gate AFTER
matchCommittedFiles so the finally block does a proper partial clear
of the just-committed files even when the note write is skipped.

0oAg (Copilot): normalizeGitCoAuthor copied value?.commit / value?.pr
without type-checking. settings.json is hand-editable; a stored
`{ commit: "false" }` reached runtime as a truthy string and behaved
as if attribution were enabled. Add a per-field bool coercion that
falls back to the schema default (true) for any non-boolean,
matching what the dialog and IDE schema already imply. Tests cover
the string / number / null cases.

0oAo (Copilot): v3→v4 shouldMigrate only special-cased versionless
legacy booleans — versionless files with invalid gitCoAuthor values
(`"off"`, `[]`, etc.) skipped the migration and the loader stamped
`$version: 4` over the bad value. Runtime normalization then
silently re-enabled attribution. Extend shouldMigrate to fire on ANY
versionless non-object value at general.gitCoAuthor; the existing
migrate() body's drop-and-warn path resets it. Already-object
shapes (hand-edited to v4) still skip cleanly. Tests added.

0oAt (Copilot): `.terraform.lock.hcl` got dropped from generated-file
exclusion when `.lock` was removed from the blanket extension list
in 3c0e3293b. It's a generated provider lockfile in the same class
as `package-lock.json` and dominates Terraform-repo commits. Re-add
to EXCLUDED_FILENAMES and add a regression test covering both
repo-root and module-nested locations.
2026-05-06 10:01:26 +08:00
wenshao
dd45e17201 fix(attribution): runGit null-on-failure, versionless v3→v4 migration
z54M (Copilot): runGit returned '' on both successful-empty-output
and silent failure, so a `--name-only` that errored mid-way through
the diff fan-out aliased to a real `--allow-empty` commit. The
empty-commit branch then preserved pending attributions, leaving
the just-committed file's tracked AI edit alive to re-attribute on
the next commit. Switch runGit to `Promise<string | null>`,
distinguishing exit code 0 (any output, including '') from non-zero
(null). The diff-stage fan-out and ancillary probes now treat null
as analysis failure and bail with `return null` instead of falling
into the empty-commit path.

z539 (Copilot): the v3→v4 `shouldMigrate` only fired on
`$version === 3`. A versionless settings file carrying the legacy
`general.gitCoAuthor: false` boolean would skip every migration
(gitCoAuthor isn't in V1_INDICATOR_KEYS — it post-dates V2), get
its `$version` normalized to 4 by the loader, and leave the
boolean in place. The settings dialog then reads the V4
`{commit, pr}` shape, sees missing keys, defaults both to true, and
silently overwrites the user's opt-out on the next save. Also fire
when `$version` is absent AND the value at `general.gitCoAuthor`
is a boolean. Tests cover the new path and confirm the existing
versioned/object-shape paths are untouched.
2026-05-06 08:48:47 +08:00
Alex Musick
174b3ac951
feat(cli): Add ability to switch models non-interactively from the cli (#3783)
* Add ability to switch models non-interactively from the cli

This fulfills request #3410

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Revert "Update packages/cli/src/ui/commands/modelCommand.ts"

This reverts commit 7b49c442a7.

* Protect against empty model name; align non-interactive behavior

Empty model names are now ignored. ```/model ``` (with trailing whitespace) will still open the interactive model picker.

Realigned the non-interactive path to use the same ```args.trim().split(' ')[0]``` logic. Valid model IDs can not contain spaces anyway. If preferred, this specific change can be reverted and the new code can use the old logic instead.

* Warn if model is not in registry

* Update command description

* Updated modelCommand test to reflect new description

* Implement auto-complete with model IDs

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Update packages/cli/src/ui/commands/modelCommand.ts

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

* Revert "Update packages/cli/src/ui/commands/modelCommand.ts"

This reverts commit 0600b2373a.

* Update modelCommand.ts

* Update modelCommand.ts

* Update modelCommand.ts

* Update/use i18n keys

* Corrected en i18n

* removed redundant .trim() on modelName check

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-05-06 00:54:27 +08:00
Yan Shen
89cb326c2f
feat(cli): improve export format completion navigation (#3701)
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
SDK Python / SDK Python (3.11) (push) Waiting to run
SDK Python / SDK Python (3.12) (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Waiting to run
* feat(cli): improve export format completion navigation

* fix(cli): address PR #3701 review feedback on /export completion

Critical:
- Guard phase-2 cycling by checking buffer text starts with "/export "
  so a manually edited buffer is never clobbered by stale nav state (C1)
- Derive export format suggestions from slashCommands.subCommands to
  keep a single source of truth with the command registry (C2)
- Reset completionSelectionWasNavigatedRef on showSuggestions rising
  edge instead of on every suggestions change to avoid a race where
  an already-navigated selection is forgotten before Enter (C3)
- Add regression tests for isPerfectMatch + navigated + Enter,
  including the positive path and a control case (C4)

Suggestions:
- Prefix-guard getExportFormatFromInput to skip regex on non-/export
  input (S1)
- Drop trailing space from setExportCompletionInput output so buffer
  text is no longer implicitly coupled to the cycling heuristic (S2)
- Document the two-phase state machine (one-shot fill + cycling) (S3)
- Accept Tab as an additional cycling key alongside Up/Down (S4)
- Remove the unconditional ref reset at the tail of handleInput;
  correctness is now guaranteed by the buffer-text guard (C1) and
  the showSuggestions edge-triggered useEffect (C3) (S5)

* fix(cli): tighten export completion cycling guard and unify Tab behavior

- Phase 2 cycling guard: replace startsWith('/export ') with strict
  getExportFormatFromInput() to prevent overwriting inputs with extra
  arguments (e.g. '/export html --verbose').
- ACCEPT_SUGGESTION in Phase 1 popup: add hasExportFormatSuggestions
  branch so Tab/Enter seeds exportCompletionSelectionIndexRef,
  allowing Phase 2 cycling to continue from the selected format
  (consistent with Up/Down arrow behavior).
- Add 4 regression tests: Phase 1 Up wrap, Phase 2 Up wrap, Tab
  seed + Phase 2 Tab cycle, guard prevents overwriting extra args.

Ref: PR #3701 second-round review by wenshao

* fix(cli): address PR #3701 third-round review feedback on /export completion

- S6: use dynamic exportFormatSuggestions.findIndex() for highlight index
  instead of static EXPORT_FORMAT_COMPLETIONS.indexOf()
- S7: derive Phase 2 cycling current index from buffer text via
  getExportFormatFromInput + indexOf, with defensive ref fallback
- S8: extract getNextExportCompletionIndex as module-level pure function;
  cache exportCycleFormats via useMemo to avoid per-keystroke .map()
- S9/S10: add tests for ESC and Ctrl+C reset of export cycling state

* fix(cli): tighten /export prefix guard, add superset matching fallthrough, and improve documentation

* fix(cli): address review #4224860127 - smaller notes optimization

- S1: Strengthen EXPORT_FORMAT_COMPLETIONS fallback comment with
  an IMPORTANT sync warning for format removals
- S2: De-export getExportFormatFromInput (no external consumers)
- S3: Add intermediate buffer-clear assertion after Ctrl+U in test
  to pin state and prevent false positives from future hook changes

* refactor(cli): extract export completion into useExportCompletion hook

Address all feedback from PR #3701 review comment:
- Extract ~310 lines of /export state machine from InputPrompt into
  dedicated useExportCompletion hook
- Replace exportCompletionSelectionIndexRef (number|null) with
  cyclingActiveRef (boolean) since index was never read
- Simplify navigated-flag lifecycle: reset on buffer.text changes
  instead of popup visibility transitions; add navigatedTextRef
  snapshot to prevent sticky autocomplete after buffer edits
- Remove static EXPORT_FORMAT_COMPLETIONS fallback; derive entirely
  from slashCommands.subCommands
- Aggregate 4 parallel ternaries into single suggestionDisplayProps
- Add regression test: navigate + backspace + retype + Enter
  should submit raw buffer, not autocomplete
- Remove redundant navigatedRef reset in ESC handler (already
  covered by exportCompletion.reset())

* fix(cli): guard export completion state
2026-05-05 23:23:25 +08:00
wenshao
3c0e3293be fix(attribution): dedup snapshot writes, cap excludedGenerated, doc commit toggle scope
rsf- (Copilot): recordAttributionSnapshot wrote a full snapshot to
the JSONL on every non-retry turn, even when the tracked state was
unchanged. Long-running sessions accumulated thousands of identical
snapshot copies, inflating session size and slowing /resume hydrate.
Dedup by JSON-equality with the prior write — first write always
goes through, identical successors are no-ops.

rsgo (Copilot): excludedGenerated path list was unbounded. A commit
churning thousands of generated artifacts (large dist/ rebuild)
could push the JSON note past MAX_NOTE_BYTES (30KB) and lose
attribution for the real source files in the same commit. Cap the
serialized sample at MAX_EXCLUDED_GENERATED_SAMPLE (50) and add
excludedGeneratedCount for the true total.

rsg9 + rshM (Copilot): the gitCoAuthor.commit description claimed
the toggle only controlled the Co-authored-by trailer, but
attachCommitAttribution also gates the per-file git-notes payload
on the same flag. Update both the schema description and the
settings.md table to mention both effects so disabling the option
isn't a silent surprise.
2026-05-05 22:52:32 +08:00
JerryLee
095a39a8d5
fix(acp): run auto compression before model sends (#3698) 2026-05-05 18:14:13 +08:00
wenshao
9e731698ae fix(attribution): submodule leak, PR body nesting, shallow-clone bail, schema default
- attachCommitAttribution: when HEAD didn't move in our cwd, leave
  pending attributions alone instead of dropping them. The case can be
  a failed commit, `git reset HEAD~1`, OR `cd submodule && git commit`
  (inner repo's HEAD moves, ours doesn't). Dropping was overly
  aggressive and silently lost outer-repo edits in the submodule case.
- addAttributionToPR: mirror addCoAuthorToGitCommit's nested-match
  rejection so `gh pr create --body "docs mention -b 'flag'"` picks the
  outer `--body`, not the inner literal `-b`. Splicing into the inner
  match would corrupt the body. Regression test added.
- getCommittedFileInfo: when `rev-parse --verify HEAD~1` fails, also
  check `rev-list --count HEAD === 1` to confirm HEAD is the true
  root commit. In a shallow clone, HEAD~1 is unreadable but the commit
  has a parent recorded — falling back to `diff-tree --root` would
  diff against the empty tree and over-attribute the entire commit.
  Bail with a debug warning instead.
- generate-settings-schema: lift `default` (and `description`) out of
  the inner `anyOf[N]` schema to the outer level when wrapping with
  `legacyTypes`. Most JSON-schema-driven editors only surface
  top-level defaults; burying the default under `anyOf` lost the
  "enabled by default" hint. Also extend the default filter to
  publish non-empty plain objects (so `gitCoAuthor`'s default can
  appear). gitCoAuthor's source default updated to the runtime shape
  `{commit: true, pr: true}` to match `normalizeGitCoAuthor`.
2026-05-05 14:16:19 +08:00
wenshao
0103af5296 Merge remote-tracking branch 'origin/main' into feat/commit-attribution
# Conflicts:
#	packages/core/src/tools/shell.ts
2026-05-05 13:38:52 +08:00
Yan Shen
59845407fc
fix(openai): parse MiniMax thinking tags (#3677)
* fix(openai): parse MiniMax thinking tags

Add a MiniMax OpenAI-compatible provider that opts into tagged thinking parsing for minimaxi.com endpoints.

Split MiniMax <think>/<thinking> content into thought parts while preserving default OpenAI-compatible behavior, and avoid rendering leading blank stream chunks as empty assistant rows.

* fix(openai): address PR #3677 review — optimize MiniMax tagged thinking parser

- perf(taggedThinkingParser): pre-compute lowercase buffer once per
  parse() call, use startsWith(tag, offset) to avoid O(N²) per-char
  slice+toLowerCase allocations
- feat(minimax): expand host matching from exact Set to *.minimaxi.com /
  *.minimax.io wildcard suffix, covering gateway / custom subdomains
- docs(converter): add comment clarifying finish_reason flush of
  buffered tagged-thinking content on stream end

* test(openai): add standalone unit tests for TaggedThinkingParser

Add taggedThinkingParser.test.ts with 23 test cases covering the boundary scenarios requested in PR review #4219047370: empty tag content, close tags in text mode, pure partial-tag-prefix chunks, multi-chunk splitting, final flag flush behavior, and case insensitivity.

* fix(openai): add observability, host guards, and reasoning_content safeguard for MiniMax tagged thinking

- Add debugLogger.warn on unclosed thought flush (observability)
- Add debugLogger.debug for tag detection and parts count
- Add cross-matching tag comment explaining binary mode toggle
- Add MINIMAX_KNOWN_HOSTS exact match layer with suffix fallback
- Add reasoning_content guard to avoid duplicating thought parts
- Add cross-matching and unclosed flush tests (+4 cases)
- Add known host exact match and custom proxy tests (+4 cases)
2026-05-05 08:52:03 +08:00
2e69d641d5
fix(core): unescape shell-escaped file paths in Edit, WriteFile, and ReadFile tools (#3820)
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-8 (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 / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(core): unescape shell-escaped file_path in Edit, WriteFile, and ReadFile tools

Shell-escaped paths (e.g. my\ file.txt) from at-completion can reach
the LLM without going through atCommandProcessor unescaping.  When
the LLM passes such an escaped path to a file tool, the backslash is
treated as a literal character in the filename, causing the tool to
fail with file-not-found.

Add defensive unescapePath() normalization at the start of each
tool validateToolParamValues(), so that escaped paths are
converted back to real filesystem paths before any I/O.

Also normalize the path in coreToolScheduler conditional-rules
injection path for consistency.

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

* fix(core): harden unescapePath — Windows safety, speculation paths, hooks, and consistency

- Make unescapePath a no-op on win32 to avoid corrupting paths like
  C:\(v2)\file.txt where backslashes are path separators.
- Apply unescapePath in speculationToolGate before overlay FS lookups
  so speculation mode finds files given escaped paths.
- Normalize file_path/path in hook toolInput so custom hooks receive
  actual filesystem paths.
- Add unescapePath to Grep, Glob, Ls, and RipGrep validateToolParamValues
  for parity with the file tools.
- Apply unescapePath in getModifyContext callbacks (Edit, WriteFile) so
  the modify-with-editor flow works when request.args still holds escaped
  paths.
- Add .trim() to Edit and WriteFile path normalization for consistency
  with ReadFile.
- Use .trim() in conditional-rules matchAndConsume for parity.
- Gate literal-backslash test to non-win32; add Windows no-op test for
  unescapePath.

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

* fix(core): persist unescaped paths in validators, cover LSP filePath, centralize modifyWithEditor normalization

- Mutate params.path in Glob/Grep/Ls/RipGrep validators so invocations
  receive the normalized path, not the original escaped form.
- Add unescapePath to LspTool.validateToolParamValues for filePath.
- Extend scheduler path normalization to filePath + notebook_path
  keys, mirroring speculationToolGate's key set.
- Move unescapePath from getModifyContext callbacks into
  coreToolScheduler's modifyWithEditor path to avoid double-unescape
  and keep a single normalization site.
- Add .trim() to speculationToolGate paths for consistency.
- Show normalized path in ls.ts validation error message.

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

* fix(core): address review feedback for unescapePath PR

- Extract shared PATH_ARG_KEYS constant to eliminate duplicate key lists
  across coreToolScheduler and speculationToolGate
- Add notebook_path to modifyWithEditor unescape block (was missing)
- Hoist unescapePath regex as module-level constant (UNESCAPE_REGEX)
- Add debug log when unescapePath actually modifies a path
- Replace early-return Windows guards with vitest skipIf in tests
- Add tests for escaped paths in Glob, Grep, Ls, RipGrep, LSP validators
- Add test for conditional rules matchAndConsume with unescaped paths

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

* fix(core): fix import order in grep.ts, consolidate ls.ts imports, win32-guard write-file tests

- grep.ts: move debugLogger declaration after all import statements
- ls.ts: consolidate two imports from ../utils/paths.js into one
- write-file.test.ts: add skipIf(process.platform === 'win32') to
  shell-escape tests (unescapePath is a no-op on win32)

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

* fix(core): address review — double-unescape, circular import, lsp asymmetry, win32 test guards

- coreToolScheduler.ts: remove redundant unescapePath in conditional-rules
  injection (toolInput was already normalized; unescapePath is not
  idempotent for \\X sequences)
- paths.ts: remove debugLogger to break circular import chain
  (paths → debugLogger → storage → paths). The single debug log line is
  low signal — dropped entirely.
- lsp.ts: remove dead .trim() checks in FILE_REQUIRED_OPERATIONS and
  RANGE_REQUIRED_OPERATIONS (filePath is already trimmed by validator)
- Add it.skipIf(process.platform === 'win32') to 11 escaped-path tests
  across edit.test.ts, read-file.test.ts, glob.test.ts, grep.test.ts,
  ripGrep.test.ts, ls.test.ts, lsp.test.ts

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

* fix(test): add win32 skipIf guards to atCommandProcessor, rulesDiscovery, fileSearch tests

Four more escaped-path tests that fail on Windows because
unescapePath is a no-op on win32:
- atCommandProcessor.test.ts: two @-command unescape tests
- rulesDiscovery.test.ts: shell-escaped match test
- fileSearch.test.ts: special-char escaping test

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

* fix(test): consolidate duplicate imports in rulesDiscovery.test.ts

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

---------

Co-authored-by: cici <cici@cicideMacBook-Pro.local>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-05 01:33:36 +08:00
jinye
2fea1d3aa7
fix(core): address post-merge monitor tool and UI routing issues (#3792)
* fix(core): address post-merge monitor tool and UI routing issues

- Guard token bucket against clock drift after system suspend/resume
  (negative elapsed resets lastRefill instead of starving the bucket)
- Add debugLogger.warn for AST read-only check failures in monitor
  getConfirmationDetails (previously silent catch)
- Consolidate SHELL_TOOL_NAMES: export from rule-parser.ts, import in
  permission-manager.ts (removes identical SHELL_LIKE_TOOLS duplicate)
- Extract hasBlockingBackgroundWork/resetBackgroundStateForSessionSwitch
  to shared backgroundWorkUtils.ts (removes identical copies in
  clearCommand.ts and useResumeCommand.ts)
- Consolidate getToolCallComponent routing into packages/webui (removes
  near-identical copies in ChatViewer.tsx and vscode-ide-companion, adds
  missing web_search compat alias to VSCode path)
- Add test for droppedLines count in terminal notification text
- Add test for exit(null, null) settlement (externally killed process)

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

* fix(core,cli,vscode): address PR #3792 review feedback

- Add debugLogger.warn for clock-drift guard (observability for
  throttle bucket resets after suspend/resume)
- Add test for clock-drift recovery (elapsed < 0 bucket reset)
- Add test for AST parse failure catch path (mockRejectedValueOnce)
- Forward isFirst/isLast props through VSCode ToolCallRouter
  (fixes timeline connector rendering)
- Add test for shell running branch in hasBlockingBackgroundWork

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

* fix(core,vscode): address follow-up review comments

- Fix React.FC missing import: use `import type { FC } from 'react'`
  instead of `React.FC` (original file had this import before refactor)
- Tighten clock-drift test: emit while clock is in the past to confirm
  guard resets lastRefill, then verify refill at the new reference point

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

* fix(core,cli): adopt review feedback — ReadonlySet + hasRunningEntries

- Type SHELL_TOOL_NAMES as ReadonlySet<string> to prevent accidental
  mutation of permission-critical set
- Use BackgroundShellRegistry.hasRunningEntries() instead of
  getAll().some() for zero-allocation short-circuit check
- Update clearCommand test mocks to include hasRunningEntries

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

* fix(core,webui,vscode): address remaining PR #3792 review comments

- Merge AgentToolCall + isAgentExecutionToolCall into single import in
  routing.ts (comment 3178280762)
- Use real getToolCallComponent via vi.importActual in VSCode test mock
  so routing logic is validated, not a parallel mock that can drift
  (comment 3178280775)
- Validate isFirst/isLast forwarding in VSCode test mock via data
  attributes (comment 3178346891)
- Add comment documenting debugLogger.warn no-op tradeoff for clock
  drift guard (comment 3178346889)

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

* test(webui): add unit test for getToolCallComponent routing

Covers all 8 component branches including the web_search compatibility
alias, agent execution detection, case-insensitive matching, and
fallback to GenericToolCall.

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

* fix(cli): add missing hasRunningEntries to useResumeCommand test mocks

The backgroundWorkUtils refactor replaced getAll().some() with
hasRunningEntries(), but the test mocks in useResumeCommand.test.ts
were not updated, causing CI failures.

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

* test(cli): add unit tests for backgroundWorkUtils shared utility

Cover hasBlockingBackgroundWork (6 cases including short-circuit
behaviour) and resetBackgroundStateForSessionSwitch (1 case verifying
all three registries are reset).

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

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
2026-05-04 21:19:41 +08:00
wenshao
d78d4cfe22 Merge remote-tracking branch 'origin/main' into feat/commit-attribution
# Conflicts:
#	packages/core/src/tools/shell.test.ts
#	packages/core/src/tools/shell.ts
2026-05-04 06:36:11 +08:00
Shaojin Wen
07fdfadc33
feat(cli): include monitors in /tasks + add interactive-mode hint (#3801)
* feat(cli): include monitors in /tasks + add interactive-mode hint

Phase B closure for Issue #3634. Two coupled changes to /tasks:

1. **Bug fix — include monitors.** The command was last touched before
   #3684 / #3791 landed, so it merged only agent + shell entries while
   monitors silently disappeared from the headless / non-interactive /
   ACP listing path. Add a third registry pull from `getMonitorRegistry()`
   and wire monitor through statusLabel / taskLabel / taskId /
   taskOutputPath. Status line includes eventCount (`running (N events)`,
   `completed (exit 0, N events)`, `completed (Max events reached, N
   events)` for auto-stop) and pid where defined.

2. **Soft deprecation hint, scoped to interactive mode only.** Once the
   richer Ctrl+T dialog (#3488 + #3720 + #3791) is available, the text
   dump is the long-form fallback rather than the primary surface. Show
   `Tip: Ctrl+T opens the interactive Background tasks dialog with
   detail view + live updates.` at the top of the output when
   `executionMode === 'interactive'`. Headless / ACP get the bare list
   — they have no dialog to point at and the hint would just clutter.
   Description string also clarified to call out the modal split.

Kept on all three executionModes (no deletion) — `/tasks` is the only
way headless / ACP / SDK consumers can inspect background-task state.

Tests: 4 new cases in tasksCommand.test.ts cover monitor entry
formatting (running with pid, natural completion with exitCode,
auto-stop with error string, failed), the singular `1 event` form,
the interactive-mode hint gating, and the cross-kind merge order.

* fix(cli): address PR 3801 review — exhaustive switch + i18n + extra tests

Three actionable Suggestions from /review's pass:

- `taskLabel` rewritten as a `switch` with a `never`-typed `default`
  arm, matching the structural-safety pattern already used by `taskId`.
  Adding a 4th DialogEntry kind in the future will now flip both
  helpers to compile errors instead of letting `taskLabel` silently
  fall through to `entry.description` (which the new kind may not have).

- Hint string wrapped in `t()` for i18n consistency with the rest of
  the file. The literal stays as the i18n key default, so today's
  output is unchanged.

- Tests: cover `cancelled` monitor status (was the only one without an
  inline assertion) and explicit `acp` execution mode hint suppression
  (pins the suppression rationale so a future regression flipping the
  check to `!== 'non_interactive'` would fail loudly).

* fix(cli): correct /tasks dialog-open hint — Ctrl+T was wrong

Tmux verification on PR #3801 caught that the hint string says "Ctrl+T
opens the interactive Background tasks dialog" but Ctrl+T is actually
bound to the MCP tool descriptions toggle (ContextSummaryDisplay.tsx
lines 110-115). The dialog opens via Down arrow on an empty composer
(focuses the footer pill) followed by Enter (InputPrompt.tsx 947-968).
Same misattribution slipped into PR #3791's first description and was
caught + fixed there before merge — this PR carried the wrong wording
forward in code.

Updates four sites:
- The hint string itself: "Tip: press ↓ from an empty composer then
  Enter to open the interactive Background tasks dialog with detail
  view + live updates."
- The slash-command description: "interactive UI is Ctrl+T" → "interactive
  dialog opens via the footer pill"
- Two inline comments referencing Ctrl+T as the dialog opener
- The interactive-mode hint test now pins on `↓` + `Enter` and
  asserts `not.toContain('Ctrl+T')` so a regression to the wrong
  wording fails loudly.

* fix(cli): address PR 3801 review — exhaustive switch consistency + path-agnostic hint

Four Suggestions from the latest /review pass:

- `statusLabel` rewritten as a single top-level switch with a
  `never`-typed default, matching `taskLabel` / `taskId` /
  `taskOutputPath`. The previous `if`/`if`/fallthrough form would
  silently apply monitor formatting to a future 4th kind.
- `taskOutputPath` gained the same exhaustive default — was the only
  per-kind helper still relying on implicit fallthrough; would
  silently omit a 4th-kind output path while the adjacent helpers
  flip to compile errors.
- Hint wording de-specifies the exact keystroke count: `'Tip: focus
  the Background tasks pill in the footer (use ↓ from an empty
  composer) and press Enter ...'`. Previous "press ↓ then Enter"
  phrasing was wrong when the Arena agent tab bar is present —
  `InputPrompt`'s focus chain routes Down through the tab bar first,
  so a single Down lands there, not on the bg pill.
- Test pin tightened: `[mon_fail] failed: spawn ENOENT (0 events)` is
  now a full-string assertion instead of a prefix match, so a
  regression that drops the `(N events)` suffix from monitor's failed
  branch fails loudly.

* fix(cli): sanitize ANSI escape sequences in /tasks output

deepseek's review pass flagged that monitor description / error fields
are user / process-supplied strings rendered directly to the terminal.
A maliciously-crafted tool description or spawn error containing raw
ANSI control sequences (clear-screen, cursor-move, colour) would
otherwise reach stdout verbatim and corrupt display.

Same risk applies to agent error / description and shell error /
command — all already-existing renderers with the same exposure that
this PR didn't introduce but inherits. So instead of per-field
sprinkling, wrap the joined output once with `escapeAnsiCtrlCodes`
(no-op when no control chars present, so cost is zero in the common
case). One line change in the renderer covers every kind including
any future one.

Test pins the behaviour: a monitor entry with `\x1b[2J` /
`\x1b[31m...` content produces output with no raw ESC bytes and
visible escaped `[...]` sequences.

* docs(cli): tighten escapeAnsiCtrlCodes comments to match actual scope

Two doc-precision Suggestions from copilot's pass on 0840e32f6:

- Source comment claimed `escapeAnsiCtrlCodes` is "a no-op when no
  control chars" but it's narrower than that — it only handles
  sequences matched by `ansi-regex` (CSI / OSC / SGR — anything
  starting with ESC). Isolated C0/C1 control bytes like BEL, BS, VT
  pass through untouched. Updated the comment to enumerate the actual
  scope and call out that `node:util`'s `stripVTControlCharacters`
  would be needed if those become a concern.

- Test comment had a literal raw ESC byte (octal 033) embedded in the
  source — visually showed `^[[...]` in editors that render ESC, but
  was a real ESC byte in the file rather than the escaped ``
  form the sanitizer produces. Rewrote with a literal `` text
  description so what the comment shows matches what the assertions
  check for.

* fix(cli): broaden /tasks sanitization + tighten inner switch exhaustiveness

Addresses 3 of 5 items from doudouOUC's PR 3801 review:

- **Issue 1 (Low) — C0/C1 control byte gap**: switched from
  `escapeAnsiCtrlCodes` (only handles ESC-initiated ANSI sequences) to
  `stripUnsafeCharacters` (one-pass strip of ANSI + VT + C0/C1, with
  TAB/CR/LF preserved). The pre-existing exposure to bare BEL / BS /
  FF / VT bytes via shell entry strings is now closed for all three
  kinds. Test rewritten to cover both ANSI sequences AND bare control
  bytes (BEL, BS), and pins that surrounding printable text and line
  breaks survive.

- **Issue 2 (Low) — inner status switches inconsistent**: the three
  inner `switch (entry.status)` blocks (agent / shell / monitor) used
  `case 'running': default: return 'running'` (or duplicated bodies).
  All three now have explicit `running` cases plus a `never`-typed
  default that throws — matches the outer `switch (entry.kind)`
  pattern and means a future status added to any of `BackgroundTaskEntry`
  / `BackgroundShellEntry` / `MonitorStatus` flips to a compile error
  here instead of silently returning `'running'`.

- **Issue 5 (Nit) — beforeEach default change**: added an inline
  comment explaining why the test default overrides
  `createMockCommandContext`'s `'interactive'` default
  (`'non_interactive'` lets the hint-suppression assertions work
  without each test rebinding context).

Issues 3 and 4 from the review are nits with no action needed (3 is
already documented as intentional; 4 is a UX call about hint length
that's better handled by user feedback than guess-tweaking).

* fix(cli): bind status to local before exhaustive switch — fixes tsc build

CI's `tsc --build` (full mode, vs `--noEmit` locally) caught that
`switch (entry.status)` followed by a `never`-typed default reading
`entry.status` doesn't compile. After the case arms exhaust the
discriminated union, TS narrows `entry` itself to `never`, so the
`.status` access in the default arm becomes "Property 'status' does
not exist on type 'never'" + the resulting `any` value can't be
assigned to `never`.

Fix: bind `entry.status` to a local `status` const before the inner
switch. The local stays typed as the per-kind status union and
narrows correctly to `never` at the default arm — `const _exhaustive:
never = status` is then `never = never`, valid.

Standard exhaustive-switch-on-discriminator pattern; doesn't change
runtime behavior or test surface, just gets past TS narrowing on the
nested case.
2026-05-03 18:45:51 +08:00
Shaojin Wen
cdadbcdb33
feat(cli): wire Monitor entries into combined Background tasks dialog (#3791)
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): wire Monitor entries into combined Background tasks dialog

Phase C mirror follow-up of Issue #3634, structurally a clean repeat of
#3720 but for Monitor (third consumer of the kind framework).

Core (MonitorRegistry):
- Add `setStatusChangeCallback` mirroring `BackgroundShellRegistry` /
  `BackgroundTaskRegistry`. Fires synchronously inside `register()` (so
  subscribers see lifecycle start) and inside `settle()` (so subscribers
  see every running → terminal transition: complete / fail / cancel /
  emitEvent's auto-stop at maxEvents).
- Subscriber failures are caught and logged but do not poison the
  registry — same defensive contract as the other two registries.

CLI:
- `useBackgroundTaskView` subscribes to all three registries (agent +
  shell + monitor) and merges by `startTime`. `DialogEntry` union
  extended with `(MonitorEntry & { kind: 'monitor' })`. `entryId`
  switches over the three kinds and returns the right id field.
- `BackgroundTasksPill.getPillLabel` adds monitor to its KIND_NAMES
  table; grouping order is shell → agent → monitor (monitor last
  because it tends to be the longest-lived, least urgent to glance at).
- `BackgroundTasksDialog`:
  - `rowLabel` returns `[monitor] <description>` for monitor rows.
  - New `MonitorDetailBody` showing command / status / pid / event
    count / dropped lines. No Progress block (monitors don't fire
    activity callbacks per-event).
  - DetailBody dispatcher gains the monitor branch.
- `BackgroundTaskViewContext.cancelSelected` routes monitor cancels via
  `monitorRegistry.cancel(monitorId)`. Monitor's cancel is synchronous
  (settle + abort happen inside the registry), matching the same path
  task_stop already uses.

Tests: 6 new core tests (registry callbacks fire on register / each
terminal transition / not on emitEvent / on auto-stop / clear stops
notifications / subscriber-failure isolation), 4 new pill tests
(singular / plural / 3-kind grouping / running-only filter), dialog
mock extended with `getMonitorRegistry`.

* fix(cli): add exhaustive default arms to DialogEntry switches

ESLint's `default-case` rule requires every switch to have a default
arm even when TypeScript can prove the union is exhaustive. Add
`default: { const _exhaustive: never = entry; throw ... }` to the
five switches added in this PR — same pattern keeps both the runtime
guard and the compile-time exhaustiveness check.

* fix(core): fire statusChange in MonitorRegistry.reset()

The newly-added `setStatusChangeCallback` subscriber misses `reset()`,
so a `/clear` or session reset leaves stale monitor rows visible in the
combined Background tasks dialog until an unrelated register/settle
event happens. Both BackgroundShellRegistry and BackgroundTaskRegistry
already fire statusChange on their reset paths — Monitor was the
outlier.

Fix: fire `statusChange()` (no arg) after `monitors.clear()`, with an
early return when the registry is already empty so we don't notify on
a no-op reset. Two new tests cover both branches.

* fix(cli,core): address PR 3791 review feedback

Four review threads from /review's second pass on top of f26b700:

1. **MonitorDetailBody live counters were stale.** `eventCount` and
   `droppedLines` came from the `useBackgroundTaskView` snapshot, which
   only refreshes on `statusChange`, and `emitEvent` deliberately
   doesn't fire `statusChange` (the per-event churn would burn the pill
   / AppContainer). Fix: extend the existing `selectedEntry` useMemo to
   re-resolve monitors from `monitorRegistry.get()` on each
   `activityTick`, mirroring the agent path. The pre-existing 1s
   wall-clock interval already drives the recompute while the entry is
   running, so no new callback wiring is needed.

2. **Settle reasons weren't persisted on `MonitorEntry`.** `fail()`,
   `complete(exitCode)`, max-events auto-stop, and idle-timeout all
   passed their reason strings only to the chat-history terminal
   notification — opening the dialog detail view after the monitor
   died showed the bare status with no clue why. Add `exitCode?` and
   `error?` fields (mirrors `BackgroundShellEntry`); populate them
   before `settle()` in each path; render them in `MonitorDetailBody`
   with appropriate colour (red for `failed`, warning for
   auto-stopped `completed`).

3. **`monitorCancel` mock had no test asserting it.** Add a dedicated
   test that opens detail on a monitor entry, presses `x`, and verifies
   `monitorRegistry.cancel(monitorId)` was called and the agent
   registry's cancel was NOT called. Pins the kind switch in
   `cancelSelected` so a regression flipping the monitor branch to
   anything else (e.g. `requestCancel`) would fail loudly.

4. **`MonitorStatusChangeCallback` docstring was wrong.** It claimed
   the callback fires on running → terminal transitions, but
   `register()` (nothing → running) and `reset()` (mass clear, fired
   with no entry) also fire it. Rewrite the docstring to enumerate the
   actual fire sites and explicitly note that `emitEvent` is excluded
   (per-event churn).

* docs(cli,core): tighten MonitorEntry.error and rowLabel comments

Two doc-precision fixes from copilot's PR 3791 review pass:

- `MonitorEntry.error` enumerated max-events as the only auto-stop
  reason that populates the field, but `idle-timeout` also writes it
  (was added in the same earlier commit). Rewrote to list both current
  reasons and explicitly note: any future auto-stop reason should
  populate this field too. Also clarified that `cancelled` and
  natural-exit `completed` paths intentionally don't.

- `rowLabel`'s shell-branch comment claimed long commands are
  "truncated by the row renderer's MaxSizedBox", but ListBody renders
  rows with plain `<Text>` (no MaxSizedBox / truncation helper). Long
  text wraps. Rewrote to describe actual behaviour and note the
  intentional decision to leave it wrapping (the dialog is what users
  open to see context — truncating defeats the purpose).

* test(cli): cover MonitorDetailBody render branches + useBackgroundTaskView

Two coverage gaps from /review's third pass on PR 3791:

- New file `useBackgroundTaskView.test.ts` (6 cases) directly exercises
  the merge logic with a stub config: empty when config is null, merges
  three registries, sorts by startTime, tags `kind`, subscribes on
  mount, refreshes when any registry fires statusChange, clears all
  three subscriptions on unmount.

- New `MonitorDetailBody render branches` describe block in
  `BackgroundTasksDialog.test.tsx` (8 cases) covers the conditional
  pieces my last commit added: title + Command, pid show/hide,
  eventCount singular vs plural, droppedLines show/hide, exitCode
  display, Error block (failed) vs Stopped because (auto-stop), and
  the all-undefined no-block case.
2026-05-03 10:05:19 +08:00
Umut Polat
a08d48b75c
fix(cli): stop double-wrapping and double-printing API errors in non-interactive mode (#3749)
* fix(cli): stop double-wrapping and double-printing API errors in non-interactive mode

In non-interactive (-p) mode, any upstream 4xx ended up on stderr three
times: once from the stream-error handler, once from handleError after
the thrown Error.message (already containing the formatted text) was
fed back through parseAndFormatApiError producing
"[API Error: [API Error: ...]]", and once more from
JsonOutputAdapter.emitResult writing the same errorMessage out in TEXT
mode. The top-level catch then framed the resulting throw as
"An unexpected critical error occurred:" with a stack trace, which
made a routine 4xx look like a CLI crash.

Three coordinated changes:

- AlreadyReportedError marks a throw whose message is already on the
  wire. handleError short-circuits on it: no second writeStderrLine,
  no second parseAndFormatApiError, just propagate the exit code.
- The non-interactive stream-error handler now throws
  AlreadyReportedError, and the catch block skips the adapter's
  emitResult in TEXT mode so we don't get a third copy.
- The top-level .catch in packages/cli/index.ts treats
  AlreadyReportedError as a routine, already-reported failure: exit
  with the carried code without printing the "unexpected critical"
  framing or the stack trace.

parseAndFormatApiError is also made idempotent — input that already
starts with "[API Error: " and ends with "]" is returned unchanged.
That is the safety net: even if a future caller forgets to mark its
throw, the double-wrap symptom is impossible.

Tests cover all three layers: idempotency in errorParsing, the
short-circuit in handleError, and a regression test on
runNonInteractive that asserts no "[API Error: [API Error: ...]" line
is ever produced on stderr.

Fixes #3748

* fix(cli): make API error formatting idempotent for 429 suffix + use AlreadyReportedError

* fix(cli): follow up on error reporting review
2026-05-03 08:39:31 +08:00
John London
5037fa7627
Feat/stats model cost estimation rebase (#3780)
* feat(stats): add optional cost estimation to /stats model

Adds optional cost estimation based on user-defined pricing in settings.json.
Users can configure per-model pricing via the new modelPricing setting.
When configured, /stats model shows estimated cost; when not configured,
the behavior is unchanged.

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

* fix(stats): extract raw model name from composite key for cost lookup

flattenModelsBySource creates keys like "model::source", but
modelPricing is keyed by raw model names. Extract the raw
model name by splitting on "::" to fix cost lookup.

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

* fix: update settings schema after adding modelPricing

Regenerate the VS Code settings schema to include the new
modelPricing field so the lint check passes.

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

* test(ui): add regression tests for cost estimation edge cases

Coverage for the cost display fixes in ModelStatsDisplay:
- Cost section hidden when no pricing + no thoughts
- Cost section shown when pricing is configured (with value check)
- Thoughts tokens included in cost calculation (with larger numbers
  to expose the before/after difference)
- Raw model name used for pricing lookup with subagent attribution
- Cost section shown when thoughts > 0 even without pricing
- Multiple models with different pricing

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

* fix(ui): include thoughts tokens in ModelStatsDisplay cost calculation

Address review feedback: the interactive cost estimate was omitting
thoughts/reasoning tokens from the output token count, causing it to
disagree with the non-interactive /stats model path.

Changes:
- hasPricing visibility gate now includes thoughts in outputTokens
- Cost estimate calculation now includes thoughts in outputTokens
- getModelName() already correctly extracts raw model name from
  flattened model::source keys for pricing lookup

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

* fix(stats): include thoughts tokens in non-interactive cost calc + update snapshots

- Fix statsCommand.ts non-interactive path to include thoughts tokens in
  outputTokens for cost calculation, aligning with the interactive
  ModelStatsDisplay path.
- Update 4 ModelStatsDisplay snapshots to reflect Cost section appearing
  when thoughts > 0 (even without pricing, showing N/A).

* fix(test): address all CI failures and reviewer feedback on PR #3780

- Add `toModelMetrics` helper to statsCommand.test.ts to properly
  create ModelMetrics with required `bySource` property
- Fix ModelStatsDisplay.test.tsx:
  - Add missing `sessionId` to SessionStatsState mock
  - Add missing `startNewSession` to useSessionStatsMock
  - Add `auto_accept` to all totalDecisions mocks (7 locations)
  - Add `files` to all SessionMetrics objects (6 locations)
  - Remove contradictory test "should show Cost section when thoughts > 0
    even without pricing" per Option A (strict opt-in)
- Revert 4 snapshots that incorrectly showed Cost/N/A lines for models
  without pricing configuration

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 07:31:08 +08:00
Shaojin Wen
9e8f826397
feat(cli): add MCP health pill to footer (#3741)
Surfaces MCP servers stuck in DISCONNECTED next to the Background tasks
pill, so a failed-to-connect or dropped MCP isn't invisible to the user
until they happen to run /mcp. v1 is a visual indicator only — Down-arrow
focus chain into the pill (Enter to open /mcp) is deferred to a follow-up
to keep this PR small and validate the right scope first.

- New `useMCPHealth` hook subscribes to mcp-client's listener API and
  exposes raw counts (total / disconnected / connecting / connected)
  rather than a pre-formatted label, so future surfaces (boot screen,
  tooltips) can derive their own presentation.
- `MCPHealthPill` component renders ` · N MCPs offline` (warning color)
  when `disconnectedCount > 0`, hidden otherwise. Connecting is
  intentionally suppressed to avoid flicker during boot/reconnect — the
  state that matters is the one that doesn't recover on its own.
- Footer renders MCPHealthPill after BackgroundTasksPill, sharing the
  same left-bottom region.

This is a deliberate parallel-pill pattern, not a `kind` extension of
the Background tasks dialog: MCP connections have no terminal status
(disconnected ↔ connecting ↔ connected, with a 30s health-check
auto-reconnect loop), so they don't fit the task contract that the
combined dialog is built around.

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-05-02 22:57:05 +08:00
jinye
df594f75fe
feat(core): event monitor tool with throttled stdout streaming (Phase C) (#3684)
* feat(core): event monitor tool with throttled stdout streaming (Phase C)

Add a new Monitor tool that spawns a long-running shell command and streams
its stdout lines back to the agent as event notifications. This is Phase C
from the background task management roadmap (#3634, #3666).

What changes:
- New MonitorRegistry (services/monitorRegistry.ts): per-monitor entry with
  lifecycle (running/completed/failed/cancelled), idle timeout auto-stop,
  max events auto-stop, AbortController-based cancellation. Follows the
  same structural pattern as BackgroundTaskRegistry.
- New Monitor tool (tools/monitor.ts): spawns via child_process.spawn with
  independent AbortController (Ctrl+C won't kill monitors), separate
  stdout/stderr line buffers, token-bucket throttling (burst=5, sustain=1/s).
  Returns immediately with monitor ID; events stream as notifications.
- Sleep interception in shell.ts: detectBlockedSleepPattern() blocks
  foreground `sleep N` (N>=2) and guides model to use Monitor or
  is_background instead.
- Config integration: MonitorRegistry instantiation, accessor, shutdown
  cleanup (abortAll), lazy tool registration.
- CLI wiring: notification callbacks in useGeminiStream.ts (interactive)
  and nonInteractiveCli.ts (headless), including hold-back loop abort on
  exit and SIGINT cleanup.

What this PR doesn't do (gated on #3471/#3488):
- Footer pill / dialog integration
- task_stop / send_message integration

Test plan:
- 21 MonitorRegistry unit tests (lifecycle, idle timeout, max events,
  XML escaping, nonexistent ID guard, callback clearing)
- 20 Monitor tool unit tests (validation, spawn, line buffering, separate
  stdout/stderr buffers, throttling, signal-killed path, turn isolation)
- 7 detectBlockedSleepPattern unit tests
- 2 E2E tests (monitor invocation, sleep interception)
- Full core suite: 248 files / 6151 passed

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

* fix(cli): hold-back loop waits for monitors + emit task_started for SDK

Two fixes from Codex review:

P1: The non-interactive hold-back loop now includes monitorRegistry.getRunning()
in its wait condition, so monitors can stream events before the CLI exits.
Previously monitors were aborted immediately after the agent's first reply.

P2: MonitorRegistry gains setRegisterCallback(), and nonInteractiveCli wires
it to emit task_started system messages. Stream-json/SDK consumers now see
a task_started for each monitor, matching the backgroundTaskRegistry contract.

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

* fix(core): Windows process kill + pipeline sleep false-positive

Two fixes from Codex review:

P1: Monitor abort handler now uses `taskkill /f /t` on Windows instead
of POSIX-only `process.kill(-pid)`. Follows the existing pattern in
ShellExecutionService.childProcessFallback.

P2: detectBlockedSleepPattern no longer uses splitCommands (which splits
on `|` pipes). Replaced with a regex that only matches sleep followed by
sequential separators (&&, ||, ;, &, newline), not pipes. `sleep 5 | cat`
is now correctly allowed.

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

* fix(test): resolve TS errors in monitor.test.ts mock types

Use Object.defineProperty for readonly ChildProcess.pid and proper
Readable type for stdout/stderr mocks to satisfy strict tsc builds.

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

* fix(core): remove false notification promise + add early-abort guard

P1: Sleep interception guidance no longer promises "completion notification"
for is_background — that wiring doesn't exist yet (follow-up from #3642).

P2: Monitor.execute() now checks _signal.aborted before spawning, preventing
a race where cancellation during tool scheduling still launches a monitor.

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

* fix(test): add getMonitorRegistry mock to useGeminiStream tests

The useGeminiStream hook now calls config.getMonitorRegistry() to wire
up monitor notification callbacks. The test mock config was missing this
method, causing 64 test failures with "config.getMonitorRegistry is not
a function".

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

* fix(test): add getMonitorRegistry mock to nonInteractiveCli tests

Same fix as useGeminiStream.test.tsx — the mock config needs
getMonitorRegistry to avoid "is not a function" errors (29 failures).

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

* fix(core): address PR review — CORE_TOOLS, directory param, test fix

1. Add 'monitor' to PermissionManager.CORE_TOOLS so coreTools allowlist
   correctly gates the monitor tool (same as run_shell_command).

2. Add optional 'directory' parameter to MonitorTool with workspace
   validation, mirroring ShellTool's directory support for multi-root
   workspaces.

3. Fix sleep-interception E2E test: readToolLogs() doesn't expose
   toolResult, so the old assertion was dead code. Now verifies via
   the model's output text instead.

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

* fix(core): address MonitorTool review #4186888042

Addresses three [Critical] review comments on packages/core/src/tools/monitor.ts:

1. Partial-line buffer unbounded growth (processLines)
   MAX_LINE_LENGTH was only enforced after a newline, so a command emitting
   a long stream without newlines would grow buffer.value without bound and
   re-split the entire accumulated string on every chunk. Now, when the
   buffer has no newline and exceeds MAX_LINE_LENGTH, we force-emit a single
   truncated event through the throttled path and reset the buffer.

2. Missing type guard on params.command
   validateToolParamValues called params.command.trim() without a typeof
   check. Schema validation normally catches this, but SDK/direct callers
   could bypass it and hit an uncaught TypeError. Added typeof === 'string'
   guard, matching the pattern used for max_events / idle_timeout_ms.

3. Workspace check bypass via raw startsWith
   The directory validator used workspaceDirs.some(d => params.directory
   .startsWith(d)), which allowed prefix collisions (e.g. /tmp/project-evil
   against a /tmp/project workspace) and skipped canonicalisation / symlink
   resolution. Switched to WorkspaceContext.isPathWithinWorkspace, which
   already does fullyResolvedPath + segment-aware isPathWithinRoot matching
   and is the standard used elsewhere in the codebase.

Test coverage: added 6 unit tests covering non-string command guard,
non-absolute directory rejection, prefix-collision rejection, traversal
rejection, workspace acceptance, and partial-line cap behaviour
(including buffer reset). All 26 monitor.test.ts cases pass.

The same startsWith pattern also exists in ShellTool and is tracked as a
separate follow-up to keep this PR focused on Phase C scope.

* fix(core): scope monitor always-allow permissions

Populate Monitor confirmation permissionRules using the same command-rule extraction path as ShellTool, so ProceedAlways persists command-scoped Bash(...) rules instead of a broad monitor-level allow. Also add unit coverage for command-scoped rules, filtering already-allowed subcommands, and extractor fallback behavior.

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

* fix(core): decouple monitor permission scope from Bash rules

Remove pm.isCommandAllowed() from MonitorToolInvocation.getConfirmationDetails()
to prevent existing Bash(...) allow rules from shrinking the monitor confirmation
scope. Monitor is a long-running background process with a different risk profile
than one-shot shell execution and should maintain its own permission boundary.
Only AST-based read-only filtering is retained.

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

* fix(core): unify monitor error/exit cleanup to prevent resource leaks

Extract a shared cleanup() helper called from both the `exit` and
`error` event handlers. Previously the `error` handler did not flush
buffers, clear buffer values, remove the abort listener, or log
dropped-line stats — causing potential memory leaks when `error` fires
without a subsequent `exit` (e.g. ENOENT for missing commands).

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

* fix(core): add user-skills-directory guard to monitor directory validation

Mirror ShellTool's getUserSkillsDirs() check in MonitorTool's
validateToolParamValues() to prevent monitor commands from running
inside user skills directories.

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

* feat(core): add Monitor(...) permission namespace for monitor tool (#3726)

Introduce a dedicated Monitor(...) permission namespace so monitor and
shell tools have independent permission boundaries. Previously monitor
emitted Bash(...) rules, causing "Always Allow" to fail for future
monitor invocations while unintentionally granting run_shell_command.

Changes:
- rule-parser.ts: add Monitor alias, SHELL_TOOL_NAMES entry,
  CANONICAL_TO_RULE_DISPLAY, DISPLAY_NAME_TO_VERB
- permission-manager.ts: extract SHELL_LIKE_TOOLS set so evaluate(),
  evaluateSingle(), hasRelevantRules(), hasMatchingAskRule() handle
  both run_shell_command and monitor
- monitor.ts: emit Monitor(...) instead of Bash(...) in permissionRules
- Tests: parseRule, matchesRule, cross-tool isolation regression,
  buildPermissionRules, buildHumanReadableRuleLabel for Monitor

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): decouple headless monitor lifetime from final result

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

* fix(cli): stabilize stream-json monitor session shutdown

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

* fix(cli): deny monitor in headless approval defaults

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

* fix(cli): honor tool aliases in headless allow checks

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

* fix(core): address opus review — sleep regex, monitor cap, non-interactive cleanup

- Fix sleep interception false positive for backgrounded sleep (`sleep 5 &
  echo done`). Remove bare `&` from separator character class so the
  background operator is not treated as a sequential separator.
- Add MAX_CONCURRENT_MONITORS (16) check in MonitorRegistry.register()
  and early rejection in MonitorTool.execute() to prevent unbounded
  process spawning.
- Widen monitorId from 8 to 16 hex chars to reduce birthday collision risk.
- Abort all running monitors in nonInteractiveCli.ts success-path finally
  so piped stdio refs don't keep the Node event loop alive after result
  emission in one-shot (--print) mode.

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

* fix(cli): abort monitors and background shells on /clear

Without this, long-running monitors from a previous session survive
/clear and continue pushing events into the new session's notification
queue. This enables cross-session prompt injection where a malicious
monitor persists across the user's escape hatch.

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

* fix(cli): abort monitors on stream-json session shutdown

Call monitorRegistry.abortAll() in both shutdown() and
drainAndShutdown() so detached monitor child processes don't survive
session termination.

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

* test(cli): use content event type in stream tests

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

* fix(cli): isolate session cleanup on clear and shutdown

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

* fix(cli): finalize session cleanup after drain

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

* fix(core): close remaining monitor review gaps

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

* fix(core): preserve shell cwd in virtual permission checks

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

* fix(core): normalize trailing background ampersands

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

* fix(core): align monitor permission and wrapper handling

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

* test(core): make monitor CI assertions cross-platform

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

* fix(core): align monitor wrapper normalization

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

* fix(core): normalize wrapped monitor commands

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

* fix(core): harden monitor headless edge cases

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

* fix(core): preserve monitor spawn errors

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

* fix(core): harden monitor register cleanup

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

* fix(core): parse monitor wrapper script token

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

* fix(core): address PR review comments for monitor tool

- Make Bash(...) permission rules cover monitor via toolMatchesRuleToolName,
  so deny rules like Bash(rm *) also block monitor({command: "rm ..."})
- Remove dead `normalizeRuleToolName` mock reference in config.test.ts
- Fix tool description to mention stdout/stderr instead of just stdout
- Export MAX_CONCURRENT_MONITORS from monitorRegistry and use it in
  monitor.ts instead of hardcoded 16
- Rename ambiguous MAX_LINE_LENGTH constants: PARTIAL_LINE_BUFFER_CAP
  (4096, monitor.ts) and EVENT_LINE_TRUNCATE (2000, monitorRegistry.ts)
- Fix schema description text: "Max 80 characters" → "Truncated to 80
  characters in display"
- Add .unref() to SIGTERM→SIGKILL escalation timer to prevent 200ms
  exit delay

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

* fix(cli): resolve clear command typecheck issues

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

* fix(core): preserve background tasks across shutdown abort

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

* fix(core): close monitor review gaps

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

* fix(core): address latest monitor review comments

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

* fix(cli): handle monitors across session switches

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

* test(core): cover aborted monitor startup

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

* fix(core): address remaining monitor PR review comments

Adopts four unresolved review threads on PR #3684:

* shell: trim top-level trailing comments before validating sleep
  separator so 'sleep 5 # wait' no longer bypasses
  detectBlockedSleepPattern.
* monitor: add sanitizeMonitorLine to strip C0/C1 control chars
  (except tab) and defang structural envelope tag names with U+200B
  before forwarding output to the model, blocking prompt-injection
  attempts hidden in monitored stdout/stderr.
* monitor: declare line buffers and throttledEmit before abortHandler
  to avoid TDZ on synchronous abort paths, and add
  flushPartialLineBuffers called from both abortHandler (before kill)
  and cleanup (natural exit/error) so partial-line data is no longer
  silently dropped on cancel.
* permissions: document that normalizePermissionContext relies on
  buildPermissionCheckContext to forward monitor's directory as cwd,
  and add regression tests proving relative-path Read(./...) allow
  and deny rules resolve against the monitor's explicit cwd.

* fix(core): abort running monitors in MonitorRegistry.reset()

reset() previously only cleared idle timers and emptied the map without
aborting running monitors' AbortControllers. This could orphan child
processes when reset() was called without a prior abortAll(), e.g. via
useResumeCommand → resetBackgroundStateForSessionSwitch.

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

* fix(core): harden monitor notification XML and displayText

- Extend escapeXml to escape " and ' as defense-in-depth: safe to reuse
  the helper in any future XML attribute context without re-auditing.
- Strip C0 (except tab) and C1 control characters from the displayText
  surface before interpolation, so untrusted child-process output cannot
  leak ANSI escapes / NUL bytes into the operator's terminal even if a
  direct caller of MonitorRegistry.emitEvent skips sanitization.

Adds unit tests for both hardening paths.

* test(core): cover token-bucket throttling and commented-sleep bypass

- Add 4 unit tests for the monitor token-bucket throttle (burst=5,
  1 token/sec refill): burst cap, refill release, long-idle bucket cap,
  and whitespace lines not consuming budget. Uses vi.setSystemTime to
  exercise Date.now() without advancing pending setTimeouts.
- Add an E2E case that feeds 'sleep 5 # wait for db' through the shell
  tool to lock in trimTrailingShellComment behavior end-to-end; the
  unit-level coverage in shell.test.ts remains authoritative but the
  E2E anchor prevents a regression from silently passing unit tests.

* fix(core): address 3 remaining copilot review comments

1. shell.ts sleep interception: strip shell wrapper before detecting the
   blocked sleep pattern so `bash -c 'sleep 5'` / `sh -c ...` cannot
   route around the block. Mirrors every other sensitive check in
   shell.ts, which already normalizes through stripShellWrapper.

2. monitorRegistry.ts emitEvent auto-stop: settle the entry BEFORE
   aborting its controller so that any synchronous abort listener that
   flushes buffered output back through registry.emitEvent() (e.g. the
   Monitor tool's flushPartialLineBuffers) finds status !== 'running'
   and short-circuits instead of overshooting maxEvents and emitting a
   duplicate 'Max events reached' terminal notification.

3. monitorRegistry.ts truncateDescription: cap output at exactly
   MAX_DESCRIPTION_LENGTH by counting the ellipsis against the budget,
   instead of returning MAX_DESCRIPTION_LENGTH + 3 characters.

Each fix is covered by a new unit test.

* fix(core): address review comments — sanitize, notify, kill logging, throttle observability

- Remove double normalize in buildPermissionCheckContext (PM is single source)
- Add {notify:false} to Config.shutdown() and abortTaskRegistries() abortAll
- Swap settle-before-abort in cancel() and resetIdleTimer() to prevent races
- Add stripDisplayControlChars to emitTerminalNotification
- Sanitize monitor description at entry creation via sanitizeMonitorLine
- Surface throttle-dropped line count in terminal notification
- Add .unref() to idle timer to allow clean process exit
- Add error handler + stdio:ignore to Windows taskkill spawn
- Log SIGTERM/SIGKILL kill failures via debugLogger.warn
- Attach early child error handler to cover spawn-to-register window
- Destroy child stdio on register failure to prevent handle leaks
- Improve stripShellWrapper to handle absolute paths, combined flags, env prefix
- Improve SHELL_TOOL_NAMES documentation and toolMatchesRuleToolName clarity

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

* fix(core): resolve monitor tool typecheck errors

- Cast child.stdout/stderr to a minimal { destroy?: () => void } shape so
  the optional destroy() call compiles and still works with test mocks.
- Initialize droppedLines: 0 in MonitorEntry test fixtures that predate
  the field becoming required.

* fix(monitor): add missing stdio option in taskkill test assertions (#3784)

* fix(core): address monitor review feedback

* fix(core): harden monitor command lifecycle

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-05-02 20:57:26 +08:00
jinye
5d1052a358
feat(telemetry): define HTTP OTLP endpoint behavior and signal routing (#3779)
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(telemetry): define HTTP OTLP endpoint behavior and signal routing

- Add resolveHttpOtlpUrl() that appends /v1/traces, /v1/logs, /v1/metrics
  to base HTTP OTLP endpoints per the OpenTelemetry specification
- Add per-signal endpoint overrides (otlpTracesEndpoint, otlpLogsEndpoint,
  otlpMetricsEndpoint) for backends with non-standard paths (e.g. Alibaba Cloud)
- Add LogToSpanProcessor that bridges OTel log records to spans for
  traces-only backends, with session-based traceId correlation and
  error status propagation
- Auto-wire LogToSpanProcessor when traces URL exists but logs URL doesn't
- Validate per-signal URLs gracefully (log error + skip, don't crash)
- Preserve query strings when appending signal paths to URLs
- Guard gRPC branch against missing base endpoint with per-signal config
- Update telemetry documentation with signal routing semantics and
  Alibaba Cloud HTTP per-signal endpoint examples

Closes #3734

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

* fix(telemetry): fix TS noPropertyAccessFromIndexSignature errors in tests

Use typed ExportedSpan interface and bracket notation for index signature
properties to satisfy strict TypeScript checks in CI.

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

* fix(telemetry): replace MD5 with SHA-256 for traceId derivation

CodeQL flagged MD5 as a weak cryptographic algorithm when used with
session.id (considered sensitive data). Switch to SHA-256 truncated
to 32 hex chars to satisfy CodeQL while maintaining the same traceId
format required by the OTel specification.

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

* fix(telemetry): address review feedback for LogToSpanProcessor robustness

- Wrap JSON.stringify in try/catch to handle circular refs and BigInt
- Add export timeout (30s) and try/catch to prevent hung shutdown
- Track in-flight exports to avoid interval-vs-shutdown race condition
- Fix deriveSpanStatus: use truthy checks (!!), drop success===false
  heuristic since declined tool calls are normal, not errors
- Enforce http(s) scheme in validateUrl to reject file:/javascript: URLs
- Change DiagLogLevel from ERROR to WARN to preserve operational diagnostics
- Preserve logRecord.instrumentationScope instead of hardcoding
- Forward severityNumber/severityText as span attributes
- Add tests for circular refs, error status edge cases, severity

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

* fix(telemetry): flush sdk shutdown through cleanup

Remove async process exit handlers from telemetry initialization and route SDK shutdown through Config cleanup so normal CLI exit paths await pending telemetry exports. Keep shutdown idempotent while an SDK shutdown is in flight.

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

* fix(telemetry): harden bridged log shutdown

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

* fix(telemetry): address review follow-ups

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

---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-01 22:47:01 +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
wenshao
98942178dc fix(attribution): address Copilot review on shell, schema, and totals
Six items called out on PR #3115 by Copilot:

- shell.ts: addAttributionToPR's bash quote escaping doesn't apply to
  cmd.exe / PowerShell, where `\$` and `'\''` aren't honored. Skip the
  PR body rewrite entirely on Windows — losing PR attribution there is
  preferable to corrupting the user-approved `gh pr create` command.

- attributionTrailer.ts + shell.ts call site: buildGitNotesCommand used
  bash-style single-quote escaping on the JSON note, which is broken on
  Windows. Switched to argv form (`{ command, args }`) and routed the
  invocation through child_process.execFile so shell quoting is bypassed
  entirely. Tests updated to assert the argv shape.

- commitAttribution.ts: when a tracked file's aiChars exceeded the diff
  --stat-derived diffSize (long-line edits where diffSize ≈ lines * 40),
  humanChars clamped to 0 but aiChars stayed inflated, leaving aiChars +
  humanChars > the committed change magnitude. Clamp aiChars to diffSize
  so the totals stay consistent.

- shell.ts parseDiffStat: only normalized rename brace notation
  (`{old => new}`). Cross-directory renames emit `old/path => new/path`
  without braces, leaving diffSizes keyed by the full string. Added a
  second normalization step.

- shell.ts: addAttributionToPR docstring claimed `(X% N-shotted)` but
  the implementation only emits `(N-shotted by Generator)`. Updated the
  docstring to match the actual behavior.

- settingsSchema.ts + generator: gitCoAuthor went from boolean to object
  in the V4 migration. The exported JSON Schema now wraps the field in
  `anyOf: [boolean, object]` (via a new `legacyTypes` hint on
  SettingDefinition) so users with a stored boolean don't see a spurious
  IDE warning before their next launch runs the migration.
2026-05-01 13:02:05 +08:00
wenshao
6f9a39c63c Merge remote-tracking branch 'origin/main' into feat/commit-attribution
# Conflicts:
#	packages/cli/src/ui/commands/tasksCommand.ts
#	packages/core/src/tools/edit.ts
#	packages/core/src/tools/shell.ts
#	packages/core/src/tools/write-file.ts
2026-05-01 12:32:22 +08:00
jinye
431a87c384
Add background agent resume and continuation (#3739)
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
* Add background agent resume support

* Fix CLI typecheck against core workspace sources

* Fix background agent resume hook and UI blocking

* Honor folder trust when resuming agents

* Fix background agent resume review follow-ups

* Fix tasks command to include background agents

* Harden background agent resume lifecycle

* Fix background task cancellation persistence

* Persist empty fork bootstrap transcripts

* Align shell prompts with managed background mode

* Guard session switches with background work

* Preserve trailing user turns during resume

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
2026-05-01 12:14:33 +08:00
易良
8b6b0d64f8
fix(cli): restore SubAgent shortcut focus (#3771)
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
2026-04-30 23:02:01 +08:00
John London
4cd9f0cbe4
feat(core): add shared permission flow for tool execution unification (#3723)
* docs: scaffold branch for #3247 tool execution unification

Placeholder commit to establish the branch for PR creation.
Actual refactoring will be done in subsequent commits.

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

* feat(core): add shared permission flow for tool execution unification

This addresses #3247 by consolidating duplicated tool execution behavior
across Interactive, Non-Interactive, and ACP modes behind shared execution
utilities.

- Add permissionFlow.ts: shared L3→L4 permission evaluation logic
- Add permissionFlow.test.ts: comprehensive test coverage (17 tests)
- Export from index.ts for use across all execution modes

Why: Permission handling logic was duplicated in CoreToolScheduler and
Session.runTool(). This shared module ensures consistent behavior across
all modes and provides a single source of truth for future fixes.

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

* fix(e2e): add bundle step to E2E workflow and fix canUseTool test

- Add 'npm run bundle' to E2E workflow so dist/cli.js exists for SDK tests
- Fix 'should handle control responses when stdin closes before replies' test:
  - Use helper.getPath() for absolute file path
  - Make prompt explicitly invoke write_file tool
  - Remove inputStreamDonePromise timeout that caused false failures
  - Add q.endInput() to signal stdin done
  - Assert canUseTool was called and file content is updated

* fix(core): wire evaluatePermissionFlow() and address PR review feedback

Address review feedback on PR #3723:
- Wire evaluatePermissionFlow() in coreToolScheduler.ts (both call sites)
- Wire evaluatePermissionFlow() in Session.ts (ACP mode)
- Delete TOOL_EXECUTION_UNIFICATION.md (had literal \n artifacts)
- Add PermissionFlowPermission union type for stronger typing
- Document the 'default' permission state in docstring
- Use needsConfirmation/isPlanModeBlocked/isAutoEditApproved helpers

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 22:10:37 +08:00
Rayan Salhab
0b7a569ac7
fix(cli): honor proxy setting (#3753)
Some checks failed
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox: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
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
* fix(cli): honor proxy setting

* fix(cli): apply settings proxy to channel start

* test(cli): cover channel start settings proxy

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-04-30 18:24:59 +08:00
wenshao
30b0039c91 Merge remote-tracking branch 'origin/main' into feat/commit-attribution
# Conflicts:
#	packages/core/src/services/chatRecordingService.ts
#	packages/core/src/tools/shell.ts
2026-04-30 17:28:38 +08:00
qwen-code-ci-bot
3f0b47172a
chore(release): v0.15.6 (#3766)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-30 15:59:35 +08:00
tanzhenxin
6c71b6b09c
chore(core): drop tool token usage tracking (#3727)
The `tool_token_count` field was sourced from `toolUsePromptTokenCount`
on the GenAI usage metadata, but none of the providers we adapt
(OpenAI/DashScope, Anthropic) populate it, and Google's Gemini API only
emits it for built-in server-side tools that qwen-code does not use.
The metric was therefore always zero in practice, so the dedicated
counter, telemetry field, UI row, and supporting plumbing are removed
end-to-end (telemetry types, OTEL counter type, UI aggregation, model
stats display, qwen-logger payload, VS Code session schema, and docs).
2026-04-30 15:35:01 +08:00
易良
23e76ff26d
fix(vscode-companion): fill slash commands into input on Enter instead of auto-submitting (#3618)
* fix(vscode-companion): fill slash commands into input on Enter instead of auto-submitting (#1990)

Previously, selecting any slash command from the VSCode completion menu
via Enter would immediately send it to the agent, giving users no chance
to append arguments. This was especially problematic for skills and
custom commands that accept parameters.

Changes:
- Commands that accept input (skills, commands with completion) now fill
  into the input box on Enter, letting users type arguments before
  submitting
- No-arg built-in commands (/clear, /doctor, etc.) still auto-submit
  on Enter for convenience
- Tab always fills without submitting (unchanged)
- Client-side commands (/auth, /account, /model) still execute
  immediately (unchanged)

The distinction is driven by the ACP `input` field: Session.ts now sets
`input: { hint }` for commands that accept arguments (non-BUILT_IN kind
or commands with completion functions), and `input: null` for the rest.

Also fixes:
- /auth + /login unified handling in useMessageSubmit.ts
- authCancelled message now clears waiting state (prevents input lockup)
- Stale /login comment updated to /auth in WebViewProvider.ts

Resolves #1990

* fix(acp): derive input field from argumentHint and subCommands, not just kind+completion

The previous logic only checked `kind !== BUILT_IN || completion != null`
to decide whether a command accepts arguments. This caused built-in
commands like /bug, /context, /export, /language, and /stats to be
marked as no-input (auto-submit on Enter), even though they accept
meaningful arguments or have subcommands.

Now a command is considered to accept input when any of:
- it is not a BUILT_IN command
- it has a completion function
- it declares an argumentHint
- it has subCommands

Also adds argumentHint to /bug since it accepts a description but has
neither completion nor subCommands.

* fix(vscode-companion): strip U+200B from completion insertion path

The contentEditable input uses U+200B as a height placeholder. When
selecting a completion item, the raw textContent was used directly for
computing trigger position and building the new text, which could
preserve the hidden character and produce text like "\u200B/commit"
that downstream slash-command handling may not recognize.

Now strip zero-width spaces from the text before computing cursor
position and trigger offsets, and adjust the cursor for any removed
characters so the final inserted text is placeholder-free.

* fix(vscode-companion): add stripZeroWidthSpaces to @qwen-code/webui mock in App.test.tsx

The test mock for @qwen-code/webui was missing the newly imported
stripZeroWidthSpaces function, causing 4 test failures in CI.
2026-04-30 15:24:47 +08:00
Rayan Salhab
f771acb353
fix(cli): persist directory add entries (#3752)
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): persist directory add entries

* fix(cli): persist accepted workspace directories

* fix(cli): handle existing workspace directories

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-04-30 11:22:04 +08:00
Fu Yuchen
2f1b52d3d3
fix(core): preserve reasoning_content in rewind, compression, and merge paths (#3579) (#3737)
* fix(core): preserve reasoning_content in rewind, compression, and merge paths (#3579)

* chore(core): remove dead stripThoughtsFromHistory methods (#3579)

* revert(pr): remove redundant reasoning merge per review feedback (#3737)

Per tanzhenxin's review: the compressed ack is plain text without
tool_calls so the thought-part injection is unnecessary, and the
converter reasoning merge is redundant given #3729's canonical
ensureReasoningContentOnToolCalls in the deepseek provider.

Both paths are now handled at the request boundary, not in history
transformation.
2026-04-30 10:25:00 +08:00
John London
b2ab751087
fix(cli): correct model precedence — argv > settings > auth env vars (#3645)
* fix(cli): correct OPENAI_MODEL precedence without breaking /model selection

Fixes model precedence regression from #3567 (reverted in #3633):

- argv.model > settings.model.name > OPENAI_MODEL > QWEN_MODEL
- settings.model.name wins over OPENAI_MODEL when it matches a provider
- OPENAI_MODEL only used as fallback when no explicit model is configured
- Added tests proving both paths: settings wins, and OPENAI_MODEL fallback works

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

* fix(cli): separate model selection from provider lookup to prevent env override

The previous implementation iterated through candidates [argv.model, settings.model.name,
OPENAI_MODEL, QWEN_MODEL] and picked the first matching provider. This caused
settings.model.name to be overridden by OPENAI_MODEL when the former didn't
match any provider.

Fix by:
1. First resolve target model using strict precedence
2. Only look up provider for that specific model
3. Filter OPENAI_MODEL/QWEN_MODEL from env when model was resolved
   from settings or argv, preventing the core resolver from picking
   up these env vars

Also fixes Edge Case 5 test (was testing buggy behavior) and adds
integration test verifying settings.model.name wins over OPENAI_MODEL.

* fix(types): remove unused type annotation from mock

The mockImplementation used ModelConfigSourcesInput type which
wasn't properly resolved. Remove the type annotation
to fix TypeScript and ESLint errors.

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

* trigger CI

* chore: trigger clean CI build

* fix(test): make mock compatible with CI type checking

The mockImplementation was causing TS2345 in CI because
the return type didn't match ModelConfigResolutionResult.
Fixed by using optional chaining and removing type annotations
that caused CI build failures.

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

* fix(test): make mock compatible with CI type checking

The mock of resolveModelConfig returned sources as { model: string }
instead of { model: ConfigSource }, causing a type error in CI only
(where strictBuild is enabled). Use proper ConfigSource objects.

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

* fix(test): update mock to match reviewer suggestion

Use proper ConfigSource objects with path/envKey details as suggested
in the review, matching what resolveModelConfig actually returns.

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

* test(cli): add regression tests for model precedence

Adds [Regression] tests that guard against the original bug where
OPENAI_MODEL incorrectly overrode settings.model.name. Tests cover:
- settings.model.name precedence over OPENAI_MODEL
- OPENAI_MODEL used when settings.model.name not set
- argv.model overriding both settings and env
- QWEN_MODEL as fallback when OPENAI_MODEL not set
- Non-OpenAI auth ignoring OPENAI_MODEL

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

* fix(cli): address reviewer feedback on model precedence PR

- Add missing assertion in Non-OpenAI auth test to verify OPENAI_MODEL
  is filtered from env passed to resolveModelConfig
- Clean up modelProvider lookup comment to clarify the old bug is fixed

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

* fix(cli): address review feedback on model precedence PR

- Sanitize OPENAI_MODEL/QWEN_MODEL in beforeEach to prevent flaky tests
- Remove stray "// trigger rebuild" comment
- Add AUTH_ENV_MODEL_VARS mapping for all auth types (not just OpenAI)
- Fix filteredEnv logic to strip ALL model env vars when model not from env
- Use sourceEnvVar tracking to only keep the env var that was actually used

Fixes the blocking test failure when OPENAI_MODEL is set in shell env.

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

* fix(cli): fix test assertion for filtered env, add source-based filtering comments

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-04-30 09:17:22 +08:00
Yan Shen
9861114ff3
fix(cli): keep sticky todo panel compact (#3647)
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): keep sticky todo panel compact

* fix(cli): stabilize sticky todo redraws

* fix(cli): address sticky todo review feedback

* fix(cli): size sticky todo number column correctly

* fix(cli): address sticky todo review feedback
2026-04-30 00:03:15 +08:00
Shaojin Wen
cae09279fa
fix(cli): bound SubAgent display by visual height to prevent flicker (#3721)
* fix(cli): bound SubAgent display by visual height to prevent flicker

The SubAgent runtime display used hard-coded MAX_TASK_PROMPT_LINES=5 and
MAX_TOOL_CALLS=5 plus character-length truncation (`length > 80`). On narrow
terminals the soft-wrapped content overflowed the available height as the
tool-call list grew, forcing Ink to clear and redraw on every update.

Pull AgentExecutionDisplay onto the same visual-height/visual-width slicing
pattern that ToolMessage and ConversationMessages already use:

- Add `sliceTextByVisualHeight` to textUtils — counts soft wraps as visual
  rows, supports top/bottom overflow direction.
- AgentExecutionDisplay now derives maxTaskPromptLines / maxToolCalls from
  the assigned `availableHeight` and uses `truncateToVisualWidth` (CJK +
  emoji safe) instead of substring(0, 80). Compact mode is unchanged.
- Drop the 300 ms debounced `refreshStatic` AppContainer fired on every
  terminalWidth change — that was a flicker source on resize and the
  static area no longer needs the refresh.

Tests:
- textUtils.test.ts covers undefined maxHeight, top/bottom overflow, and
  soft-wrap counting.
- AgentExecutionDisplay.test.tsx asserts the height-bounded render keeps
  the prompt + tool list inside the assigned rows.
- AppContainer.test.tsx asserts width-only changes no longer clear the
  terminal.

* test(tui): add SubAgent flicker regression script and ANSI counter

Two reusable tools for measuring TUI flicker:

- `scripts/measure-flicker.mjs` — standalone Node script that counts the
  ANSI escape sequences which betray flicker (clearTerminalPair, clearScreen,
  eraseLine, cursorUp) inside any recorded raw stream (`script` log,
  `tmux pipe-pane` output, custom PTY capture). Supports baseline diff mode.

- `integration-tests/terminal-capture/subagent-flicker-regression.ts` —
  end-to-end ratchet that boots a mock OpenAI server, drives a real qwen
  process through an `agent` tool dispatch + 5 `read_file` SubAgent rounds,
  then reads PTY bytes and asserts ANSI-redraw counts stay below configured
  ceilings. Mirrors PR #43f128b20's resize-clear-regression pattern.

Reference numbers (60-col / 18-row terminal, fixed build):
  clearTerminalPair=5, clearScreen=10, eraseLine=440, cursorUp=132

The ratchet defaults to 10/20 ceilings — roughly 2× steady state — so
regressions like reverting sliceTextByVisualHeight or restoring the
width-driven refreshStatic trip the build.

Implementation notes captured in the script's docstring:
  - Strips HTTP_PROXY family env vars (NO_PROXY isn't honored by undici,
    so corp proxy would otherwise hijack the loopback request).
  - Drops `--bare` (bare mode hard-codes the registered tool set and
    rejects the `agent` tool); HOME is sandboxed to a temp dir instead.
  - Mock server speaks SSE because the CLI requests stream:true.

* fix(cli): address inline review on SubAgent flicker fix

Three issues from inline review on PR #3721:

1. **availableHeight as total budget (Critical).** The previous formula
   only constrained prompt + tool-call height, not the surrounding
   header / section labels / gaps / footer. Default and verbose mode
   could still overrun the parent-provided budget. Subtract a fixed-row
   overhead (10 rows running, 18 completed) before computing
   `maxTaskPromptLines` / `maxToolCalls`. Add unit tests that assert the
   rendered frame line-count stays within `availableHeight` for both
   running and completed states.

2. **Ratchet that actually distinguishes fix from no-fix.** The previous
   `clearTerminalPair` / `clearScreen` ceilings passed for both fixed
   and unfixed builds. Add an `eraseLine` upper bound (default 460) —
   that's the metric whose drop reflects the in-place-update efficiency
   the visual-height fix delivers (no-fix observed 469, with-fix 434).
   Refresh docstring with the current numbers and a coverage map that
   honestly states what this ratchet does and does not exercise.

3. **Keypress scope.** `useKeypress` was active on every mounted
   `AgentExecutionDisplay`, including completed/historical instances in
   chat history — Ctrl+E / Ctrl+F would toggle them all in lock-step
   and cause large scrollback reflows. Gate `isActive` on
   `data.status === 'running'`. Test mock now also honors
   `{ isActive }` so the new "completed displays ignore Ctrl+E"
   regression is enforceable.

* fix(cli): address round-2 inline review on SubAgent flicker

Three follow-up issues from inline review on PR #3721:

1. **sliceTextByVisualHeight reservedRows early-return (Critical).**
   The early return compared `visualLineCount <= targetMaxHeight` and
   ignored `reservedRows`, so a caller asking us to keep one row free
   for a footer could still receive the full input back with
   `hiddenLinesCount: 0` even though only `targetMaxHeight - reservedRows`
   content rows were actually available. Compare against
   `visibleContentHeight` instead and add a regression test for the
   `'a\nb\nc' / 3 / reservedRows: 1` case the reviewer flagged.

2. **Footer hint and rendered prompt now share one slicing result
   (Suggestion).** Previously `hasMoreLines` looked at
   `data.taskPrompt.split('\n').length` (hard newlines only), but the
   prompt body was already truncated by `sliceTextByVisualHeight` (which
   counts soft wraps). A long single-line prompt could be visually
   truncated without the footer ever surfacing the "ctrl+f to show
   more" hint. Lift the slice into the parent component and feed both
   the rendered `TaskPromptSection` and the footer's `hasMoreLines`
   from the same `hiddenLinesCount`.

3. **Running → completed transition test (Critical).** The previous
   "completed displays ignore Ctrl+E" test rendered already-completed
   data, so `useKeypress` was inactive from the start and Ctrl+E was a
   no-op trivially. It missed the real path: a running subagent gets
   expanded, then completes while preserving the expanded
   `displayMode` — which is exactly when the completed-state budget
   has to hold the layout. Replace the test with a `rerender`-based
   one that runs the full transition, asserts the completed expanded
   frame stays within `availableHeight`, and asserts the post-transition
   Ctrl+E is a no-op. Bumped `COMPLETED_FIXED_OVERHEAD` from 18 to 22
   to accommodate the ExecutionSummary + ToolUsage block accounting
   that the new transition test exposed.

* fix(cli): gate SubAgent useKeypress on isFocused for parallel runs

Per @yiliang114's review on PR #3721 — `data.status === 'running'` alone
fixes the historical/scrollback case but two SubAgents running in parallel
both stay `running`, so a single Ctrl+E / Ctrl+F still toggles them in
lock-step and the dual reflow brings back the flicker the gating was meant
to prevent. The component already receives `isFocused` from ToolMessage
(via SubagentExecutionRenderer) for the inline confirmation prompt — reuse
it on the keypress hook:

  isActive: data.status === 'running' && isFocused

Adds a regression test that renders a running SubAgent with
`isFocused={false}` and asserts Ctrl+E is a no-op (frame unchanged).

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-29 22:34:55 +08:00
顾盼
65a1503e13
fix(memory): use project transcript path for dream (#3722)
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
* fix(memory): use project transcript path for dream

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

* fix(memory): quote dream transcript grep path

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

* test(memory): make dream prompt quoting test portable

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-29 17:56:53 +08:00
Shaojin Wen
7b3d36e1f3
feat(cli): wire background shells into combined Background tasks dialog (#3720)
* feat(cli): wire background shells into combined Background tasks dialog

Phase B follow-up #2: surface managed background shells in the same
overlay that already shows local subagents, so users get one unified
view instead of having to remember /tasks for shells.

- BackgroundShellRegistry: add setRegisterCallback/setStatusChangeCallback
  and requestCancel(id), mirroring BackgroundTaskRegistry's contract.
  register() also fires statusChange so subscribers see the lifecycle
  start, not just transitions.
- useBackgroundTaskView: subscribe to both registries, merge entries by
  startTime, attach a `kind` discriminator (DialogEntry union) so
  renderers can dispatch on agent vs shell.
- BackgroundTasksPill: group running counts by kind ("2 shells, 1 local
  agent"); when all entries are terminal, collapse to "N task(s) done".
- BackgroundTasksDialog: replace per-kind section header with a single
  "Background tasks" header; ListBody renders shell rows as
  "[shell] <command>"; DetailBody dispatches to AgentDetailBody (the
  original) or a new ShellDetailBody (cwd / output file / pid / exit).
- Context cancelSelected switches by kind: agents go through cancel(),
  shells through requestCancel() — only aborts, lets the spawn settle
  path record the real terminal state (mirrors task_stop in #3687).

Tests: 8 pill cases (singular/plural per kind, mixed, terminal-only),
4 dialog cases (auto-fallback on running→terminal, cancel flow,
already-terminal stays in detail, selectedIndex clamp); shell registry
gains 5 callback tests + 3 requestCancel tests.

* fix(cli): refresh detail-body agent fields between status changes

useBackgroundTaskView shallow-copies agent entries into DialogEntry so
each entry can carry a `kind` discriminator. The copy detaches
`recentActivities` from the registry: BackgroundTaskRegistry.appendActivity
mutates `entry.recentActivities = next` on the registry object and emits
`activityChange`, but the dialog's activity callback only bumps a local
counter — so the snapshot's `recentActivities` reference goes stale and
the Progress block keeps rendering the old array until the next
status-driven refresh.

Resolve `selectedEntry` against the registry on each render when the
selected entry is an agent, with `activityTick` as a useMemo dep so it
recomputes on every activity callback. Snapshot remains the source of
truth for the list (no churn on the pill / AppContainer); only the
detail body re-reads live.

Also rename the non-empty list section header from "Local agents" to
"Background tasks" to match the empty-state branch and the unified
multi-kind contents.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-29 16:06:36 +08:00
pomelo
2ee014e347
fix(cli): refresh static header on model switch (#3667)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-29 07:16:26 +08:00
Ird
8896f93fe9
mcp config as cli (#1279)
* mcp config as cli

* fix: failed test cases

* updated tests to match the new api

* fix: update mcp-config tests for new API and fix type import

* mcp config type declared handler.ts

---------

Co-authored-by: Ird <irdali.durrani@fixstars.com>
Co-authored-by: mingholy.lmh <mingholy.lmh@alibaba-inc.com>
2026-04-29 07:00:33 +08:00
易良
8de1bcb279
chore(release): bump version to 0.15.3 (#3708)
Some checks failed
Qwen Code CI / CodeQL (push) Waiting to run
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled
Update all package versions from 0.15.2 to 0.15.3 across the monorepo
including root package.json, package-lock.json, and all sub-packages
(channels, cli, core, vscode-ide-companion, web-templates, webui).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-28 21:04:52 +08:00
tanzhenxin
6763124a05
fix(cli): preserve description in subject-bearing thought chunks (#3691)
When a streamed reasoning chunk arrived with both a parsed subject (from
**Title**) and a description (the body text after \n\n in the same chunk),
the Thought event handler routed only to setThought and discarded the
description. As a result, the first body word that happened to share a
chunk with the closing ** was dropped from the persistent reasoning
display.

Treat subject-only chunks as discrete loading-indicator updates and route
all chunks carrying streamed text through the throttled buffer. The
existing flush merger preserves the subject across batched events.
2026-04-28 16:32:59 +08:00
tanzhenxin
e973dabf37
test(cli): remove flaky TUI input tests surfaced by CI history mining (#3694)
These eight tests share the same shape — stdin.write(...) followed by a
fixed setTimeout while waiting for React state to settle — and have
failed across multiple unrelated commits on slow CI runners (Windows and
Ubuntu 24.x). Mining the last 60 failed CI runs showed each one failing
on 2-10 distinct SHAs, with low fails-per-SHA — the classic signature
of a timing flake rather than a real bug.

Removed:
- AuthDialog > should trigger OpenRouter OAuth from API key options
- AuthDialog Custom API Key Wizard > navigates to protocol selection
  when Custom API Key is selected
- AuthDialog Custom API Key Wizard > navigates to base URL input after
  selecting a protocol
- AuthDialog Custom API Key Wizard > calls handleCustomApiKeySubmit on
  Enter in review view
- AuthDialog Custom API Key Wizard > shows advanced config screen after
  entering model IDs
- AuthDialog Custom API Key Wizard > passes generationConfig when
  advanced options are toggled
- InputPrompt > prompt suggestions > accepts and submits the prompt
  suggestion on Enter when the buffer is empty
- SessionPicker > Preview Mode > opens preview on Space and closes on Esc

The behaviors are still covered by adjacent tests in the same suites.
2026-04-28 16:32:48 +08:00
Shaojin Wen
aac2e96ec3
feat(core): managed background shell pool with /tasks command (#3642)
Some checks are pending
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
Qwen Code CI / Lint (push) Waiting to run
* feat(core): managed background shell pool with /bashes command

Replace shell.ts's `&` fork-and-detach background path with a managed
process registry. Background shells now have observable lifecycle, captured
output, and explicit cancellation — matching the pattern used by background
subagents (#3076).

Phase B from #3634 (background task management roadmap).

What changes
- New `BackgroundShellRegistry` (services/backgroundShellRegistry.ts):
  per-process entry with status (running / completed / failed / cancelled),
  AbortController, output file path. State transitions are one-shot
  (terminal status sticks; late callbacks no-op). Mirrors the lifecycle
  shape of #3471's BackgroundTaskRegistry so the two can be unified later.
- `shell.ts` is_background path rewritten as `executeBackground`:
  - Spawns the unwrapped command (no '&', no pgrep envelope)
  - Streams stdout to `<projectDir>/tasks/<sessionId>/shell-<id>.output`
    (path layout aligns with the direction sketched in #3471 review)
  - Bridges the external abort signal into the entry's AbortController so
    a single source of truth governs cancellation
  - Returns immediately with id + output path; agent's turn isn't blocked
  - Settles the registry entry asynchronously when ShellExecutionService
    resolves: complete (clean exit) / fail (error) / cancel (aborted)
- Removes ~120 lines of dead bg-specific code from shell.ts:
  pgrep wrapping, '&' appending, Windows ampersand cleanup, Windows
  early-return path, bg PID parsing, tempFile cleanup
- New `/bashes` slash command: lists registered shells with id, status,
  runtime, command, output path. Empty state prints a friendly message.

What this PR doesn't do
- Footer pill / dialog integration — gated on #3488 landing
- task_stop / send_message integration — gated on #3471 landing
- Auto-backgrounding heuristics for long foreground bash — Phase D

Test plan
- 11 registry unit tests (state machine + idempotent terminal transitions)
- 4 background-path tests in shell.test.ts (spawn no-wrap + complete /
  fail / cancel settle paths)
- 2 /bashes command tests (empty + populated)
- Full core suite: 247 files / 6075 passed (existing tests unaffected)

* fix(core): address PR #3642 review feedback

Three [Critical] from the auto review + naming alignment with Claude Code:

- shell.ts settle: non-zero exit code or termination signal now bucket into
  `failed` instead of `completed`. The previous `if (result.error) fail else
  complete()` would misreport `false` / failed `npm test` as success because
  ShellExecutionService surfaces ordinary command failures as a non-zero
  exitCode with `error: null`. Failure reason carries the exit code or signal
  so `/tasks` shows the real cause.

- ShellExecutionService.childProcessFallback: add `streamStdout` mode that
  emits each decoded chunk through the existing onOutputEvent path. The
  default (foreground) path continues to buffer + emit the cleaned final
  blob, so existing in-line shell calls are unaffected. executeBackground
  opts in via `{ streamStdout: true }`, which is what makes the captured
  output file actually useful for long-running processes (dev servers,
  watchers) — without it the file stayed empty until the process exited.

- shell.ts test fixture: cancel-settle test was using `signal: 'SIGTERM'`
  but `ShellExecutionResult.signal` is `number | null`. TS2322 broke the
  build; switched to `signal: null`. Added a test that explicitly covers
  the new "non-zero exit → failed" path so the bucketing change has
  regression coverage.

- shell.ts comment: explicitly document why background shells force
  `shouldUseNodePty=false` (no terminal, no human; node-pty would be dead
  weight for fire-and-forget commands).

- /bashes → /tasks (alias bashes), description "List and manage background
  tasks" — matches Claude Code's command name. Currently lists shells only;
  will surface other task kinds (subagents, monitor) as those registries
  land via #3471 / #3488.

* fix(core): address PR #3642 second-round review feedback

- shellExecutionService streaming: drop stdout/stderr buffer + outputChunks
  accumulation in streaming mode. Each decoded chunk goes straight to
  onOutputEvent and is GC-eligible immediately. Long-running background
  commands (dev servers, watchers) no longer accumulate unbounded memory
  proportional to total output. Buffered (foreground) mode is unchanged.

- shell.ts executeBackground: stripAnsi each chunk before writing to the
  output file. Dev servers / build tools spam color codes and cursor-move
  sequences that would render as garbage in the file the agent reads.

- bashesCommand: command description "List and manage" → "List background
  tasks" — current implementation only supports listing, cancellation
  follows when the unified task_stop tool from #3471 is wired in. Replace
  the hand-rolled formatRuntime helper with the shared formatDuration
  utility (uses hideTrailingZeros for parity with the previous output).

- backgroundShellRegistry: add a comment documenting the lack of an
  eviction policy as a known limitation. LRU / age-based / capped-size
  eviction (and on-disk output rotation) is left as a follow-up alongside
  the broader output-file lifecycle story.

* fix(core): address PR #3642 third-round review feedback

- shell.ts executeBackground: add 'error' listener on the output write
  stream. fs.createWriteStream surfaces write failures (disk full,
  permission, fs going away) as 'error' events; without a listener Node
  treats it as an uncaught exception and kills the entire CLI session.
  Log + drop is the sane default — the registry still settles via
  resultPromise so /tasks shows the right terminal status.

- shell.ts executeBackground: store the abort handler reference and
  removeEventListener in the settle callback. Background shells outlive
  the turn signal; the dangling listener was keeping `entryAc` (and
  transitively `outputStream`) reachable until the turn signal itself was
  GC'd, which for long sessions would never happen.

- shell.test.ts: extend the createWriteStream mock with an `on` stub so
  the new error-listener wiring doesn't crash the test suite.

* refactor(cli): drop /bashes alias and rename file to tasksCommand

Per follow-up review: the slash command should be exclusively /tasks.
Removes the `bashes` altName, renames `bashesCommand{,.test}.ts` →
`tasksCommand{,.test}.ts`, renames the exported binding `bashesCommand`
→ `tasksCommand`, and cleans up the remaining `/bashes` references in
backgroundShellRegistry.ts comments. No behavior change beyond the
alias removal.

* refactor(cli): finish tasksCommand rename — apply content changes

The previous commit (03c8503c8) only captured the file rename via
`git mv`; the export name change (`bashesCommand` → `tasksCommand`),
the removal of `altNames: ['bashes']`, the import update in
BuiltinCommandLoader, and the `/bashes` → `/tasks` comments in
backgroundShellRegistry.ts were unstaged when that commit landed.
Squash candidate before merge.

* fix(core): address PR #3642 fourth-round review feedback

Four reviewer concerns from @wenshao + @doudouOUC:

- [Critical] Config.shutdown() now also calls
  `backgroundShellRegistry.abortAll()`. Previously only the subagent
  registry was aborted, so a managed background shell could outlive the
  CLI process and orphan its child. Symmetric with how
  `BackgroundTaskRegistry.abortAll()` is wired in.

- [P1] shell.ts executeBackground strips a trailing `&` from the command
  before spawn. The managed path is itself the backgrounding mechanism;
  forwarding `node server.js &` verbatim made bash exit immediately while
  the real child outlived the wrapper, causing the registry to settle as
  `completed` while the shell was still running and chunked output to
  land on a closed stream. Strip + warn.

- [P2] Output file moves under `storage.getProjectTempDir()` (specifically
  `<projectTempDir>/background-shells/<sessionId>/shell-<id>.output`).
  `ReadFileTool` already auto-allows the project temp dir, so the LLM
  can `Read` the captured output without bouncing off a permission
  prompt — important because background-agent contexts can't surface
  interactive prompts.

- [P2] Background shells are no longer killed when the current turn's
  AbortSignal fires. Forwarding the turn signal into the entry's
  AbortController meant a Ctrl+C on the turn would also terminate
  intentionally backgrounded dev servers / watchers, contradicting the
  independent-lifecycle promise. Cancellation now flows only through
  `entryAc` (driven by future `task_stop` integration via #3471).

Tests:
- New `abortAll` registry tests cover running / mixed / empty cases.
- `runs background commands as managed pool entries` test stops asserting
  the wrapper-vs-entry signal identity since they're now structurally
  separate (no turn-to-entry forwarding).
- New `does not forward the turn signal into the background shell` test
  pins the new behavior.
- New `strips trailing & from the spawned command` test pins the strip.
- Removed the cancel-via-outer-signal settle test — that path no longer
  exists; cancellation is exercised end-to-end via the registry's own
  `cancel` and `abortAll` tests in `backgroundShellRegistry.test.ts`.

* fix(core): tighten trailing & strip — narrow regex + ReDoS-safe

Two reviewer concerns on the same line of #3642 round 4:

- [Critical CodeQL] `\s*&+\s*$` is a polynomial-time regex on
  uncontrolled input (long all-`&` strings backtrack quadratically).
- [P2 doudouOUC] `&+` is too greedy: it also rewrites `npm run dev &&`
  into `npm run dev` (breaks logical AND syntax) and `echo foo \&` into
  `echo foo \` (eats the escaped literal). Only the bare bash background
  operator should be stripped.

Replace the regex with a small linear-time helper
`stripTrailingBackgroundAmp` that explicitly checks for the three
"don't touch" cases (`&&`, `\&`, no trailing `&`). Plain `endsWith` /
`slice` — no regex backtracking, and the intent reads off the page.

Tests:
- Existing strip-trailing-`&` test still passes.
- New `does not strip a trailing &&` test pins the logical-AND case.
- New `does not strip an escaped trailing \\&` test pins the escape case.

* fix(core): keep binary-detection sniff in streaming mode

@doudouOUC noted that `streamStdout` shortcut returned before the
binary-sniff path, so a background command emitting binary bytes
(`cat /bin/ls`, image dump, etc.) would be text-decoded and appended
to the task output file unbounded.

Restructure handleOutput so the sniff-and-cutover logic runs in both
modes:

- Both modes accumulate up to MAX_SNIFF_SIZE for the binary check.
  The accumulator is bounded; once the threshold is reached, it stops
  growing in streaming mode (dropped on binary detection / left
  inert on text confirmation) and continues to accumulate in buffered
  mode (existing foreground behavior).
- Streaming mode emits 'binary_detected' as soon as `isBinary` trips
  so the consumer can stop writing the output file. Up to ~4KB of
  bytes may have been emitted as text chunks before detection — this
  is bounded and acceptable; the unbounded write is the pathology
  reviewers flagged.
- Streaming text mode still emits each decoded chunk immediately and
  does not accumulate stdout/stderr strings, so long-running text
  streams remain GC-friendly.
- Buffered (foreground) behavior is unchanged — the sniff accumulator
  is the same path the existing tests cover.

Tests: 50 shellExecutionService + 11 backgroundShellRegistry + 57
shell.test.ts all pass; no regressions.

* fix(core): tighten streaming sniff bound + Windows rmSync flake

Two unrelated reds on the latest CI run:

1. [P1 doudouOUC] Streaming sniff buffer leaks on small chunks.
   The previous fix recomputed `sniffedBytes` from
   `Buffer.concat(outputChunks.slice(0, 20)).length` on every chunk —
   pinned to the first 20 chunks. If those total under MAX_SNIFF_SIZE
   (line-sized stdout, e.g. dev-server logs) the byte count never grew,
   the sniff branch stayed open forever, and `outputChunks` accumulated
   every later chunk — exactly the leak `streamStdout` was meant to
   prevent.

   Track sniffed bytes by running sum (`sniffedBytes += data.length`)
   so the bound is genuine. When sniff confirms text in streaming mode,
   drop the accumulator immediately so subsequent chunks fall through
   the streaming emit path without ever touching it.

2. file-exporters.test.ts afterEach `fs.rmSync` flaked on Windows
   (ENOTEMPTY: directory not empty). The exporter's underlying write
   stream hasn't always released its handle by the time `rmSync` runs.
   Pass `maxRetries: 5, retryDelay: 50` so the cleanup retries through
   the brief Windows handle-release window instead of failing the test
   on a CI quirk.

---------

Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
2026-04-28 11:06:50 +08:00
tanzhenxin
03c88b7308
feat(cli): background-agent UI — pill, combined dialog, detail view (#3488)
* feat(cli): background-task UI — pill, combined dialog, detail view

Adds the user-facing surface for background tasks on top of the
model-facing agent control primitives merged in #3471. A dedicated
pill in the footer summarises running tasks, ↓ focuses it, and Enter
opens a combined dialog listing every task with a detail view that
shows the original prompt, live stats, and a rolling progress feed
of recent tool invocations.

Also renames BackgroundAgent* to BackgroundTask* for consistency with
the user-facing terminology and the task_* tool family.

* chore: trigger CI
2026-04-28 10:57:59 +08:00