Every gate compose-review enforces proves the agents READ the diff — coverage
proves the transcripts, anchors prove the quotes — but none proves the review
could tell good code from bad. Dogfooded: a weak-model run drafted nothing
from its entire roster on a diff where stronger same-condition runs found a
verified blocking Critical, and compose printed a bare, confident
"Verdict: Approve". A reader takes that as evidence of quality when it is only
absence of signal — the most dangerous shape a review verdict can have.
Disclose it, deterministically. When the composed event is APPROVE and the
plan's srcDiffLines — the same field the review topology is chosen from, with
tests, docs and generated files excluded by construction — exceeds 100, the
verdict line names the shape:
Verdict: Approve — low signal: none of the 11 review agents reported a
finding on a non-trivial diff (155 source diff lines)
The event never moves on it: a cap would punish every genuinely clean diff,
and nothing was in fact found. Docs-only and typo-class diffs keep their bare
Approve — there, finding nothing is the expected outcome. The floor sits well
past the typo-fix class (a tiny edit scattered one line per hunk stays under
it) and at a fifth of the smallest diff the topology gate calls big. Both
numbers in the line are the run's own: the roster the plan required (all on
record at APPROVE, or coverage would have capped) and the plan's source-line
count.
Co-authored-by: verify <verify@local>
* fix(cli): stop resize repaint from causing scroll storm (#8004)
Remove the useResizeSettleRepaint -> refreshStatic wiring that wrote
clearTerminal (destroying scrollback) and remounted <Static> on every
settled resize, re-emitting all conversation history in 50-item chunks.
Ghostty's panel-toggle animation exceeds the 200ms debounce, triggering
multiple settle-repaint cycles per toggle -- visible as continuous
scrolling/flickering.
Ink's dynamic region already re-renders on width changes via
useTerminalSize; modern terminals handle scrollback reflow natively.
The full remount is no longer necessary. The now-unused hook and its
test are removed (no remaining callers).
* test(cli): guard resize no-repaint contract against settle-time regression (#8004)
* test(cli): make resize settle regression test non-vacuous (#8004)
The previous test used rerender() which remounts the tree via ink's
ErrorBoundary (measureElement returns undefined → layout effect throws
→ tree unmounted), so the settle debounce never fired and the test
passed regardless. Rewrite to keep the tree alive (measureElement mock
returns a real value) and deliver width changes to the same mounted
instance via a listener pattern. Mutation-verified: fails when the
removed useResizeSettleRepaint hook is restored.
* chore(cli): remove dead useResizeSettleRepaint from eslint legacy filenames (#8004)
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Anthropic's Messages API rejects any tool `input_schema` carrying a
top-level `oneOf`/`allOf`/`anyOf` combinator with a hard 400:
input_schema does not support oneOf, allOf, or anyOf at the top level
send_message's schema used `oneOf: [{ required: ['to'] }, { required:
['task_id'] }]` to express "either a teammate recipient or a
background-task id is required." Since this schema is forwarded
verbatim as `tools[].input_schema` to Anthropic
(anthropicContentGenerator/converter.ts -> convertSchema), every call
to send_message on an Anthropic-backed model (native API, Vertex, or
an Anthropic-compatible proxy) failed outright -- not degraded, fully
unusable.
Verified live against the Anthropic Messages API (claude-sonnet-5):
the schema with `oneOf` 400s with the exact message above; the
identical request with `oneOf` removed succeeds (200, tool_use
returned).
The `oneOf` constraint was redundant defense-in-depth, not the only
enforcement: send_message's execute() already returns a clear runtime
error ("No active team and no task_id provided...") when neither `to`
nor `task_id` is supplied. Dropping the schema-level constraint leaves
that runtime validation intact and unchanged for OpenAI/Gemini-backed
models, while making the tool actually callable on Anthropic-backed
models.
Fixes#7984
Co-authored-by: Palanisamy, Dinesh <Dinesh.Palanisamy@netapp.com>
* feat(channels): add pairing approval management API
* fix(sdk): expose pairing approval types
Re-export the new approval and revocation types from the public SDK entry, and pin the qualified workspace DELETE request body in regression coverage.
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
* feat(channels): add GitLab polling channel adapter
Poll GitLab todos via @gitbeaker/rest, dispatch notes through the
existing PollingChannelBase pipeline. Key design points:
- action_prompt_template config drives event filtering and metadata
rendering (unconfigured actions are skipped)
- Per-repo cursor (repo[chatId].last_read) as notes window lower bound,
global lastProcessedAt for todo-level dedup
- mark_done after successful processing; failure skips mark_done for
retry on next poll
- Mention gating delegated to base GroupGate (adapter only sets
isMentioned flag)
- First-contact body fallback for todos with no notes (e.g. mention in
issue description)
* fix(channels/gitlab): persist cursor after each successful todo
Call saveCursor() immediately after advancing lastProcessedAt so that
progress is durable even if the process crashes mid-poll. Also removes
the local watermark variable in favor of direct assignment.
* fix(channels/gitlab): persist cursor on every advancement including skips
* fix(channels/gitlab): address review critical issues
- Remove non-functional proxyAgent (gitbeaker doesn't support it)
- Construct repo_url from host + path (API doesn't return web_url)
- Handle directly_addressed action (falls back to mentioned template)
- First-contact fetches target description instead of using todo.body
- Move todo.project dereference inside try block
- Filter confidential notes
- Update channel-registry.test.ts for gitlab entry
* fix(channels/gitlab): address review suggestions
- Warn on connect if action_prompt_template is not configured
- Guard todo.target.iid before use
- Skip paths now mark_done (best-effort) to clean GitLab UI
- Remove postErrorComment (avoids duplicate comments on retry)
- Fetch only first page of notes (desc, maxPages:1, perPage:100)
instead of paginating entire note history
- Extract fetchRecentNotes for single-page windowed enumeration
* refactor(channels/gitlab): simplify to todo.body dispatch, add description mention support
- Remove notes API fetching; dispatch todo.body directly
- Detect description mentions via target_url anchor (#note_ absence)
- Always fetch target description for %description% metadata
- Remove per-repo cursor; dedup via cursor + mark_done only
- Cursor advances regardless of success/failure (no retry)
- Use zod for cursor validation
- Rename template vars to GitLab terminology:
%project% %project_url% %target_type% %iid% %title% %description% %todo_id%
- Support %% escape for literal percent
* docs(channels): add GitLab adapter documentation
- New user guide: docs/users/features/channels/gitlab.md
- Update _meta.ts navigation
- Update developer adapter matrix and SDK list
* fix(channels/gitlab): use correct Issues.show(issueIid, { projectId }) signature
* chore: regenerate NOTICES.txt for new gitlab channel dependencies
* fix(channels/gitlab): address review suggestions
- Add todo.project null guard (item 2)
- Single-pass regex for %% escape + %var% substitution (item 4)
- sendThreadMessage throws directly on undefined threadId (item 5)
- Dedup fetchDescription with per-poll cache (item 6)
- Remove per-todo saveCursor; base class saves after pollOnce (item 7)
- Add undefined threadId test (item 8)
- Expand confidential notes limitation in docs (item 3)
* test(channels/gitlab): add mention tests, directly_addressed coverage, skip assertions, temp cleanup
- New mention.test.ts: 14 cases for testBotMention/stripBotMention/escapeRegex
- Add directly_addressed fallback test
- Skip tests now assert TodoLists.done + cursor advancement
- afterEach cleans up mkdtempSync temp dirs
* fix(channels/gitlab): address review round 4
- Non-mention actions (assigned, etc.) set forceMentioned=true to bypass GroupGate
- Merge dead note-filter tests into single 'skips todo authored by bot'
- Log fetchDescription errors to stderr instead of silent swallow
- Post error comment on issue/MR when handleInbound fails (best-effort)
* fix(channels/gitlab): always force isMentioned=true, remove regex re-derivation
The action_prompt_template config is already the event filter, and
GitLab has already decided the mention when creating the todo.
Re-deriving isMentioned via regex on todo.body causes permanent
message loss when the regex misses (description mention + fetch
failure, group mentions). Always set forceMentioned=true so
GroupGate never drops a todo that passed the template filter.
* fix(channels/gitlab): propagate fetchDescription errors for description mentions
For note mentions, description is metadata-only — fetch failure is
logged and swallowed. For description mentions, description IS the
message — fetch failure now propagates to the outer catch, which
posts the ⚠️ error comment so the user knows to re-mention.
* perf(channels/gitlab): clean up stale todos, skip unnecessary fetchDescription
- Mark stale todos (updated_at <= cursor) as done on each poll to
prevent perpetual re-fetching of pre-existing pending todos
- Skip fetchDescription for note mentions when template does not
contain %description%, saving one API call per todo
- Update docs: stale todo cleanup, error comment on failure
* docs(channels/gitlab): clarify requireMention is bypassed, template is the real filter
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): use todo ID cursor instead of timestamp to eliminate equal-timestamp loss
Timestamp-based cursors (second granularity) could silently destroy
todos sharing the same updated_at as the cursor boundary. Switch to
monotonically increasing todo IDs which are unique and collision-free.
Add initialized flag to preserve first-start drain semantics: pre-existing
pending todos are marked done without dispatch on the first poll cycle.
* fix(channels/gitlab): harden first-poll drain, add ordering tests, fix lockfile
- Replace Math.max(...spread) with reduce to avoid RangeError on large
backlogs (~100k+ todos). Move initialized=true after the drain work so
any throw retries the drain instead of falling through to dispatch.
- Add unit tests: identical-timestamp delivery and id-order-when-updated_at-disagrees
(kills M2 sort mutant).
- Align lockfile: file:../base → ^0.21.0 for channel-base dep.
* fix(channels/gitlab): include dot in mention lookahead for GitLab usernames
GitLab usernames may contain dots (e.g. bot.name). The lookahead
character class inherited from GitHub omitted '.', causing @bot.name
to match as @bot. Add '.' to the negated class.
* docs(channels/gitlab): align docs with ID cursor and drain semantics
- Add first-poll drain as step 2 in How It Works
- Clarify GroupGate always passes (isMentioned forced true)
- Document initialized flag in Known Limitations
* Apply suggestions from code review
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(channels/gitlab): align package version and channel-base dependency to 0.21.1
Bump version from 0.21.0 to 0.21.1 to match other channel packages after
upstream merge. Pin @qwen-code/channel-base to exact 0.21.1 instead of
^0.21.0, matching the convention used by other published channels.
* fix(channels/gitlab): regenerate lockfile to match package.json versions
Manually add only gitlab-related lockfile entries (workspace, @gitbeaker
packages, transitive deps, channel-gitlab link) without unrelated npm
normalization churn.
* test(channels/gitlab): add regression tests for first-poll drain hardening
Two tests that kill the M1 (Math.max spread RangeError) and M2 (flag
ordering) mutants which survived the original 46-test suite:
- 150k todo drain verifies reduce() handles large backlogs without
RangeError and without dispatching
- Drain throw verifies initialized stays false so the next poll retries
the drain instead of falling through to dispatch
Test file duration: ~40ms → ~170ms.
* docs(channels/gitlab): clarify groupPolicy must be "open" and add runtime warning
The default groupPolicy "disabled" silently drops all mentions — todos are
marked done and cursor advances, but no dispatch occurs. Fix misleading docs
that said "GroupGate always passes" (only true at groupPolicy: "open") and
add a connect()-time warning when groupPolicy is not "open".
* fix(channels/gitlab): correct xcase integrity hash in lockfile
The manually added xcase entry had a typo in the sha512 hash (ys → ks),
causing npm ci EINTEGRITY failures in CI.
* fix(channels/gitlab): correct requester-utils integrity hash in lockfile
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(channels/gitlab): allow groupPolicy "allowlist" in warning and docs
The groupPolicy warning and docs incorrectly stated that groupPolicy
must be "open". In reality "allowlist" with the project listed also
works because isMentioned is forced true and GroupGate only requires
the group to be listed. Also fix the inaccurate "no error is logged"
claim — ChannelBase logs preflight rejected reason=group_disabled.
Fixes R5-🟡3 from PR #7862 review.
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(web-shell): add split pane header action slot with overflow
Let hosts render per-session actions in each split pane header, collapsing them into a … menu when the pane is too narrow.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(web-shell): add pane header actions PR screenshots
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): tighten pane header overflow measurement
Drop the per-render children effect dependency that rebuilt ResizeObserver during streaming, and reserve workspace-tag width when computing available header space.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review blockers
Mount host actions in only one tree, and wrap overflow entries as DropdownMenuItems so Radix selection and keyboard navigation work.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): keep pane header actions alive across overflow
Flatten Fragment host actions before building the overflow menu, and keep the same host instances mounted when collapsing so stateful actions are not reset.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address pane header overflow review suggestions (#7808)
* fix(web-shell): proxy overflow clicks via action slots
Wrap host pane actions in stable slots so the overflow menu can activate interactive descendants without requiring opaque custom components to forward internal data attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shell): address overflow menu review suggestions (#7808)
* fix(web-shell): harden pane header overflow actions (#7808)
Restore the 8px gap between the built-in maximize/close controls, ignore
aria-hidden glyphs when labelling overflow items, omit non-interactive
children from the overflow menu, and document the popover constraint on
renderHeaderActions. Refreshes the design doc to match the mount-once
implementation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes
HTTP hooks hard-block all private/link-local address ranges via ssrfGuard,
which makes them unusable in platform-managed environments where the hook
receiver is a first-party, VPC-internal endpoint (e.g. an internal API
gateway resolving to 172.16.0.0/12).
Add an opt-in setting, security.allowPrivateNetworkHooks (default false),
that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs)
and validateResolvedHost (literal + post-DNS-resolution paths).
Security properties:
- Honored only from User/System/SystemDefaults scopes; the value is
stripped from Workspace settings during the merge (with a startup
warning), so a cloned repository can never self-grant the bypass.
- BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains
blocked even when the flag is on.
- Default false keeps every code path byte-for-byte compatible with
current behavior; bare/safe mode forces it off.
* fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks
Address review findings on #7968: with the flag on, cloud metadata
endpoints were reachable through gaps in the relaxed checks.
- ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and
isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms
(::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing
extractMappedIPv4/expandIPv6Groups helpers.
- urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP
isMetadataAddress check now run unconditionally; only the general
range check (isBlockedAddress) is relaxed by the flag.
- httpHookRunner.validateResolvedHost: no longer returns early with the
flag on — DNS resolution still runs and resolved addresses are checked
against isMetadataAddress, so a hostname resolving to a metadata
endpoint is blocked. DNS failures still defer to fetch, as before.
- settings warning text now lists User/System/SystemDefaults, matching
the schema and docs.
- docs: precise wording — the flag relaxes only range checks; metadata
endpoints stay blocked in all serialized forms and after DNS resolution.
The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata
endpoints (169.254.169.254, 100.100.100.200 in any form, plus the
BLOCKED_HOSTS hostnames) are unreachable in every configuration.
---------
Co-authored-by: 欢伯 <ri.xur@alibaba-inc.com>
* fix(cli): MCP prompt completion no longer blocks Enter for optional params (#7991)
The completion handler treated all unused arguments (required + optional)
as completion suggestions with auto-appended `="`, making optional params
look mandatory. After selecting a prompt name, pressing Enter would
auto-append `--input="` instead of executing the prompt.
Two changes:
1. Completion handler now parses named args directly instead of using
parseArgs() (which returns Error on missing required args — the normal
state during tab completion).
2. Only required unused args are suggested as completions. When all
remaining unused args are optional, the completion list is empty so
Enter executes the prompt with defaults.
* fix(cli): restore optional and positional MCP prompt completion discovery (#7991)
* fix(cli): share named-arg regex and cover mixed MCP prompt completion (#7991)
* fix(cli): suggest optional args mid-keystroke and test multi-word positionals (#7995)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
* fix(cli): reserve Ctrl+Shift+C for terminal copy, not quit/clear
Ctrl+Shift+C is the standard terminal copy shortcut. The QUIT and
CLEAR_INPUT bindings matched it because they only constrained ctrl
and left shift unspecified, which the matcher ignores. In Kitty-
protocol terminals the escape-hatch check also caught Ctrl+Shift+C
(ESC[99;6u) since it tested ctrl+name without excluding shift.
Add shift: false to both bindings and !key.shift to the escape-hatch
condition so Ctrl+Shift+C passes through to the terminal for copy.
Plain Ctrl+C (shift undefined or false) is unaffected.
Fixes#8006
* test(cli): guard Ctrl+Shift+C paste escape-hatch against !key.shift mutation
The existing Kitty Ctrl+Shift+C test passed identically with or without
the `!key.shift` condition in the Ctrl+C escape hatch, so a regression
making Ctrl+Shift+C clear a stuck paste would ship undetected. Add a
test that enters a stuck keypress-level paste and asserts Ctrl+Shift+C
is buffered as paste content rather than dispatched, which fails when
`!key.shift` is removed.
* fix(cli): remove dead !key.shift escape-hatch guard (#8011)
The KeypressContext.tsx escape-hatch guard added in bb86d57 is
unreachable: Node readline never produces a Key with ctrl=true,
name='c', and shift=true for any terminal encoding (kitty,
modifyOtherKeys, or legacy 0x03). Mutation test M2 confirmed
reverting this hunk has zero effect on the test suite.
Remove the guard and the two tests that exercised it. The
keyBindings.ts shift:false fix (the load-bearing change proven by
M1) and its keyMatchers tests are untouched.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Dogfooded on a live review: with ~2.7G free, `npm ci` on this monorepo ran 33
seconds and died on ENOSPC — and the now-full disk went on to fail every agent
scheduled after this command. Verification and reverse-audit agents could not
even spawn, so a Critical that nine agents independently reported ended the run
capped at Comment because nothing could verify it. A disk a command fills is
not a failure that stays contained to that command.
build-test already treats "cannot finish in time" as an infrastructure result:
the deadline skips the command and discloses it, and the agent brief routes
such notes to informational, never a finding. "Cannot fit on the disk" is the
same class of result, discoverable before the command runs instead of 33
seconds into it.
Add a free-disk preflight at two floors: 3 GiB before `npm ci` (the installed
tree is ~1.4G and npm stages cache and temp writes on the same filesystem
while materialising it) and 1 GiB before the build phase (dist/ and
tsbuildinfo write far less; the floor exists so a compile cannot be the thing
that fills the disk). Two floors deliberately: a warm tree at ~2G free skips
the install anyway and must still be allowed to build. Below a floor the
command is skipped with the same disclosure shape as a deadline skip. Where
statfsSync is unavailable the preflight passes — it exists to prevent
failures, not to invent them.
Co-authored-by: verify <verify@local>
* fix(cli): map Kitty Super (Command) modifier to meta
Kitty-protocol terminals forward Cmd+C as a CSI-u sequence whose modifier
parameter carries the Super bit (8), which the parser silently dropped. The
key then parsed as a bare printable "c" (meta: false) and leaked into the
input box even though the terminal performed the copy itself. Fold the Super
bit into the emitted meta flag at every Kitty modifier decode site so
Super-modified keys are no longer inserted as text.
Fixes#7990
* test(cli): cover Super-bit fold on reverse-tab and functional-keys paths (#7996)
* test(cli): make functional-key Super-bit case load-bearing
Use Home (ESC [ 1 ; 9 H) instead of Up arrow (ESC [ 1 ; 9 A): readline
claims modified arrows before the Kitty arrowPrefix decoder, so the arrow
case passed even without the Super-bit fold. Home exercises the decoder
and fails on unfixed code.
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
The DingTalk interactive cards change (#6930) added concurrent
session-cancellation coalescing to DaemonSessionClient, growing the
minified browser daemon bundle to 180295 bytes — 71 bytes over the
176KB budget, breaking npm run build on main. Bump the budget to
177KB following the established pattern.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): allow bounded reads of large text files
* fix(serve): bound large-text reads by scan cost, not by which knob was set
Follow-up to the bounded large-text read path. Three changes:
Gate on any explicit window argument, not on `limit`. Gating on `limit`
had the cost model backwards in both directions: `{ line: 900_000_000,
limit: 20 }` was admitted despite walking the whole file, while
`{ maxBytes: 4096 }` — satisfiable from the first 4 KiB — was refused. A
read with no window argument at all still fails, since a caller that
believes it holds the whole file may write it back truncated.
Add MAX_TEXT_SCAN_BYTES (8 MiB). MAX_READ_BYTES caps what a read
returns; nothing capped what it cost. Line offsets are resolved by
scanning from byte 0, so a query param could turn into an
uninterruptible multi-second scan of an arbitrarily large file — and on
Windows hold a read handle for that span, blocking renames and deletes.
Past the budget the read is refused with `file_too_large` pointing at
readBytes, which reaches any offset in O(1).
Tolerate appends on streamed windows. Requiring whole-file size/mtime
stability after reading a prefix rejected reads whose returned bytes
were still valid, and the case it rejected — tailing a live log — is the
one this path exists for. Streamed windows now assert inode identity
plus "did not shrink"; truncation and replacement are still rejected.
Also: non-UTF-8 large text now returns `binary_file` rather than
`file_too_large`, so a client retrying on 413 with a smaller window
can't loop forever; and `readFileWithLineAndLimit` throws instead of
silently ignoring a caller-supplied `fileHandle` on the by-path
fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(serve): harden large text range snapshots
Treat caller-owned file handles as bounded streaming reads, cap them to the captured file size, and reuse the chunk buffer.
Restore strict Serve snapshot stability and align returned-slice metadata with the full-snapshot path.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(serve): make large text ranges snapshot-safe
* fix(serve): harden large-text ctime tests and document buffer reuse (#7947)
Address review feedback on the large-text range read PR:
- Pause before restoring mtime in the two ctime-dependent mutation tests so the change-time advances past the pre-read snapshot even on coarse-resolution filesystems, removing a latent flake in the same-size-overwrite precondition. The assertions are unchanged.
- Document at the readFileHandleChunks yield site that the 512 KiB buffer is reused across iterations, so yielded views must be decoded or copied before advancing the generator.
* docs(serve): soften same-size rewrite guarantee to coarse-clock best-effort (#7947)
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
On Windows (pasteWorkaround path), shouldFlushRawDataAsPaste
misclassifies raw stdin data containing both a carriage return (0x0d)
and an SGR mouse sequence as pasted content. The synthetic paste
wrapper sets isPaste=true in handleKeypress, which discards SGR mouse
data via resetSgrMouse(), breaking wheel scrolling in VP mode.
This commonly triggers when Enter (\r) and a subsequent mouse wheel
event arrive in the same stdin chunk — a frequent occurrence on
Windows Terminal. macOS is unaffected because handleStdinData does
not use this heuristic.
Skip the paste heuristic when the buffer contains ANSI escape bytes
(0x1b) so SGR mouse sequences always reach readline for proper
parsing and dispatch to mouse subscribers.
Closes#7964
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
* fix(cli): correct hardware cursor off-by-one in fullscreen mode
In fullscreen mode Ink omits the trailing newline, so after writing
output the terminal cursor sits ON the last line rather than one past
it. buildCursorSuffix assumed the latter and emitted one extra
cursorUp, placing the hardware cursor (used for IME positioning) one
row above the software block cursor — visible as an extra white
rectangular segment protruding above the input line.
Pass a hasTrailingNewline flag through buildCursorSuffix,
buildReturnToBottom, buildCursorOnlySequence, and
buildReturnToBottomPrefix in the Ink patch so both the standard and
incremental log-update renderers compute the correct cursor movement.
Closes#7980
* fix(cli): revert incorrect hasTrailingNewline in buildReturnToBottom
Self-review caught that buildReturnToBottom does NOT need the
hasTrailingNewline adjustment: its previousLineCount argument comes
from str.split('\n').length, which already includes the trailing
empty element when the output ends with '\n'. The original formula
(previousLineCount - 1 - y) is the exact inverse of the corrected
buildCursorSuffix in both fullscreen and non-fullscreen modes.
Only buildCursorSuffix needs the flag, because its visibleLineCount
argument excludes the trailing empty element.
* fix(cli): sync cursor-helpers.d.ts and add fullscreen cursor-only test (#7998)
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
* fix(web-shell): preserve session URL context
* fix(web-shell): keep daemon token out of the session URL (#7926)
Restore the daemon token stripping that was dropped alongside the
base-path fix: removeDaemonTokenFromUrl() on startup and the ?token=
delete in replaceStandaloneSessionUrl. The ?token= query path is still
supported for backward compatibility, so without stripping it leaks into
the address bar, history, access logs, and Referer headers.
Also extract the session pathname building into buildSessionPathname()
with unit tests covering root/sub-path deployments, the no-session case,
trailing slashes, and id encoding.
* fix(web-shell): anchor session URL parser to agree with writer (#7926)
Extract parseSessionId() next to buildSessionPathname() and anchor it to the last /session/<id> segment so the parser agrees with the greedy writer. Previously a base path ending in a session segment produced /app/session/session/<id>, which the first-match parser read back as the literal id "session". Add round-trip and trailing-slash coverage.
* test(web-shell): cover parseSessionId malformed-encoding catch branch (#7926)
* fix(web-shell): preserve base path in split-view URL (#7926)
---------
Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(core): mark folders the item budget never expanded
A folder is pushed into its parent's subFolders and queued for expansion.
When it is later dequeued and the maxItems budget is already spent, the
BFS skips it with a bare `continue`, leaving hasMoreFiles and
hasMoreSubfolders unset. It then formats as a bare leaf -- byte for byte
how a genuinely empty directory formats.
So the same tree was described differently depending only on the budget.
The existing suite shows it: `level1/level2/level3/file.txt` renders
level3 with its file at maxItems 10, and as an empty directory at
maxItems 3. The subfolder test is worse -- folder-0 through folder-3 each
contain child.txt and all four render as empty.
The parent's hasMoreSubfolders flag does not cover this. It means "I could
not add every subfolder", and these folders were added; it is only the
expansion that never happened.
Set hasMoreSubfolders on the skipped node so it renders with the
truncation indicator. Only that one flag is set: formatStructure emits an
indicator per flag, and this one alone produces the single trailing `...`.
Three existing expectations encoded the old rendering and are updated.
* docs(core): state both meanings of hasMoreSubfolders on the interface
This PR widened the field: it already meant "subfolders were truncated" and
now also means "the item budget ran out before this folder was read." The
interface comment only described the first, so a second consumer reading just
the type could gate rendering on `subFolders.length > 0` and silently drop the
indicator for never-expanded folders, reintroducing the same
looks-like-an-empty-directory bug in a new path.
* fix(core): improve ripgrep runtime reliability
Make ripgrep failures distinguishable from true no-match results so the
model avoids unsafe conclusions from partial or failed searches. Add a
narrow recovery path for confirmed worker-thread EAGAIN failures.
- Retry confirmed thread EAGAIN once with a single worker thread
- Treat exit code 1 as no-match only when both streams are empty
- Mark partial runtime results as incomplete, separate from display limits
- Add privacy-safe telemetry and focused coverage for recovery behavior
# Conflicts:
# packages/core/src/utils/ripgrepUtils.test.ts
* fix(core): restore ripgrep runtime recovery tests
Restore the runRipgrep coverage that was lost during the rebase and keep
EAGAIN detection aligned with the runtime reliability design.
- Re-add coverage for strict no-match handling and incomplete output
- Verify single-thread retry behavior for confirmed EAGAIN failures
- Keep spawn, cancellation, timeout, and max-buffer paths covered
- Treat os error 11 as the short EAGAIN marker documented by the plan
* docs(core): document ripgrep recovery boundaries
Clarify the non-obvious runtime reliability edges around ripgrep recovery so
future changes preserve the intended narrow behavior.
- Document why only confirmed worker EAGAIN failures are retryable
- Explain incomplete-output handling for interrupted ripgrep output
- Note the privacy boundary for runtime recovery telemetry
* fix(core): correct exit-1 no-match gate for --json summary output (#7888)
* test(core): remove duplicate mockReset and add telemetry coverage (#7888)
* fix(core): address review feedback on ripgrep recovery semantics (#7888)
- Fix stale `truncated` property in test mock to match RipgrepRunResult
- Narrow `incomplete` flag to genuinely interrupted executions only
- Add test for exit code 1 with both stdout and stderr
- Add test for EAGAIN retry producing partial stdout
- Alias RipgrepRuntimeRecoveryFailureKind to RipgrepFailureKind
* docs(core): align exit-1 no-match spec with stderr-only gate (#7888)
* fix(core): mark exit-failed ripgrep searches with stdout as incomplete (#7888)
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
* feat(core): integrate Goal turn engine
* test(core): preserve Goal tool isolation in agent overrides
* fix(core): pause Goal at Stop hook cap
* fix(core): keep Goal recovery from blocking sessions
* fix(core): degrade failed Goal migration writes
* fix(core): reset loop detector in Goal stop hook continuation (#7895)
The goal-runtime stop hook continuation path was missing
loopDetector.reset(prompt_id) before recursing, causing tool calls
to accumulate across iterations and trip TURN_TOOL_CALL_CAP after
a handful of healthy iterations. The non-goal stop hook path already
had this reset.
Also simplifies the redundant conditional in Turn.run() — the two
near-identical sendMessageStream calls are collapsed into one since
sendMessageStream already handles an undefined goalContext internally.
* fix(core): align turn.test.ts assertion with unified sendMessageStream call (#7895)
* fix(core): preserve error cause in GoalPersistenceUnavailableError (#7895)
---------
Co-authored-by: qwen-code-dev-bot <269191875+qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
* feat(web-shell): add git branch picker, commit dialog, and create PR flow
Add an IntelliJ-style branch picker popover to the web shell git
workspace, accessible from the branch chip in both the composer toolbar
and sidebar. The picker provides search-filtered branch listing (local,
remote, tags, recent), branch checkout, new branch creation, pull, push,
and a commit view integrated into the existing GitDialog.
The commit view reuses the diff panel (expandable file diffs with syntax
highlighting, fullscreen support) and adds a commit message textarea with
Commit / Commit and Push buttons. When a session is available (or one is
auto-created), the commit message and PR title/body are generated via the
model using session side-queries (btwSession), giving the agent full
conversation context for accurate generation.
The Create PR flow provides an inline form with auto-detected base branch
and model-generated title/description, backed by a new daemon route that
shells out to gh pr create.
New daemon routes:
- GET /workspaces/:workspace/git/branches
- POST /workspaces/:workspace/git/checkout
- POST /workspaces/:workspace/git/branch
- POST /workspaces/:workspace/git/push
- POST /workspaces/:workspace/git/pull
- POST /workspaces/:workspace/git/commit
- POST /workspaces/:workspace/github/prs/create
- GET /workspaces/:workspace/github/default-branch
* fix(web-shell): resolve correct workspace session for AI generation
The commit message and PR title/body generation now resolves the
most recent session for the target workspace via listWorkspaceSessions,
rather than using the globally active connection.sessionId which may
belong to a different workspace or session. Falls back to creating a
new session only when no sessions exist for the workspace.
Also improves the PR body editor with an Edit/Preview toggle using
the existing Markdown component, and updates the generation prompt
to follow the project PR template structure from AGENTS.md.
* fix(web-shell): stabilize session resolver prop to prevent infinite re-generation
Pass resolveSessionForWorkspace as a stable useCallback reference
instead of an inline arrow function. The inline function created a
new reference on every App render, causing the GitDialog useEffect
to abort and restart generation in an infinite loop.
* fix(web-shell): group remote branches by remote name and add PR target branch dropdown
Remote branches in the branch picker are now grouped by remote
(origin, upstream, etc.) with sub-headers, making fork workflows
clear. The PR create form's base branch field is now a select
dropdown populated from the workspace's branch list, grouped by
remote with optgroup labels, instead of a free-text input.
* fix(web-shell): use ref for session resolver to prevent effect re-run abort
When resolveSessionForWorkspace creates a new session, it updates
connection.sessionId in the provider, which changes the useCallback
reference, which triggers the useEffect to re-run and abort the
in-flight btwSession generation. Store the callback in a ref so the
effect never depends on its identity.
* fix(web-shell): resolve session per workspace, not from global active session
The commit/PR generation effects used connection.sessionId directly
without checking if it belongs to the target workspace. When opening
the commit dialog from a sidebar workspace different from the active
session's workspace, the wrong session was used for generation,
producing incorrect content. Now always routes through
resolveSessionForWorkspace(workspaceCwd) which checks workspace
membership before reusing the active session.
Also replaces 'PR' with 'Pull Request' / '合并请求' in all UI strings
and adds error logging to the generation catch blocks.
* fix(web-shell): retry btwSession with fresh session when stale session detected
When listWorkspaceSessions returns a session that no longer exists in
the daemon's memory (e.g. after daemon restart), btwSession fails with
'No session with id ...'. The generation effects now catch this error,
force-create a new session via resolveSessionForWorkspace(cwd, true),
and retry the btwSession call once. Both btwWithRetry and
resolveSessionForWorkspace are stored in refs to avoid useEffect
dependency chain aborts.
* fix(web-shell): base PR generation on branch diff, not working tree
PR title/body generation now fetches the commit log between the
resolved base branch and HEAD (git log <base>..HEAD) plus any
uncommitted changes, instead of only the working tree diff. The
base branch is resolved inside the effect's promise chain (not
from state) to avoid stale values and dependency warnings. Also
adds range parameter support to fetchGitLog and workspaceGitLog.
* feat(web-shell): replace PR base branch select with searchable popover
The native <select> for the PR target branch is replaced with a
custom searchable popover (BranchSelect) that shows a search input
and a filtered branch list grouped by remote. The default selection
is the target repository's main branch (resolved via
getDefaultBranch). Supports filtering by typing in the search box.
* fix(web-shell): show full remote ref in branch select (origin/main)
Branch select now stores and displays full remote refs like
origin/main instead of stripped names. getDefaultBranch returns
the full ref (origin/main) instead of stripping the prefix.
When creating the PR, the remote prefix is stripped for the
gh pr create --base flag (origin/main → main).
* fix(web-shell): stop pointer propagation in branch picker list to prevent popover dismiss
Radix Popover's outside-click detection was incorrectly firing when
clicking section headers (Recent/Local/Remote/Tags) inside the popover
content, causing the popover to close immediately. Adding
onPointerDown stopPropagation on the list container prevents pointer
events from reaching Radix's document-level handlers.
* fix(web-shell): use onPointerDownOutside guard for branch picker popover
The previous stopPropagation approach failed because Radix Popover
uses capture-phase document listeners for outside-click detection,
which fire before bubble-phase stopPropagation. The correct fix is
onPointerDownOutside on PopoverContent: when Radix incorrectly fires
the outside handler for a click that is actually inside the content
(can happen with portal containers), we check contentRef.contains()
and preventDefault to keep the popover open.
* fix(web-shell): stop click propagation on branch picker popover content
Root cause: the ChatEditor composer container has onClick that calls
core.focus(), stealing focus from the popover. React synthetic events
bubble through the React tree (not DOM tree), so portaled popover
clicks reach the container handler. Radix then detects focus-outside
and dismisses the popover.
Fix: onClick stopPropagation on PopoverContent, matching the existing
pattern in GitModePopover and ToolbarPopover which already have this
fix with an explanatory comment.
* docs: add PR verification screenshots for branch picker feature
* fix(web-shell): mock useWorkspace in tests for BranchPickerPopover
BranchPickerPopover calls useWorkspace() which requires
DaemonWorkspaceProvider context. The existing WorkspaceSection and
ChatEditor tests didn't provide this context, causing 5 test failures.
Added vi.mock with importActual to preserve other exports while
providing a mock useWorkspace. Also updated the git chip click test
to reflect that clicking now opens the branch picker popover instead
of directly calling onOpenGitDiff.
* fix: harden git write paths against argument injection
Address review feedback on the web-shell git surface:
- Reject option/pathspec injection in git checkout ref and branch start
point (isValidCheckoutRef), and terminate `git checkout` argv with `--`.
- Drop `git log` range values that start with `-` and terminate the argv
with `--` so a range can never be reinterpreted as `--output=<file>`.
- Fix getDefaultBranch always falling back to origin/main: the promisified
exec lacked `encoding: 'utf8'`, so stdout was a Buffer and .trim() threw.
- Parameterize ghErrorMessage so `gh pr create` timeouts name the right
command and duration; sanitize workspace paths in PR-create errors.
- GitDialog: guard doCommit against double-submit (button + keyboard), and
strip only a known remote prefix from the PR base so local branches with
"/" are not mangled; use theme tokens for commit button/success colors.
- BranchPickerPopover: guard checkout/new-branch behind busyAction, reset
inline-input text on reopen, and hide the commit action when unavailable.
Adds regression tests for the checkout/branch validation and the git log
range guard.
* fix(web-shell): address review feedback on branch picker PR (#7731)
- Fix Commit+Push error masking: split try/catch so push failure
reports alongside the successful commit SHA
- Replace hardcoded screenshot path with captureScreenshot harness
- Replace silent if-isVisible skip with explicit assertion in visual test
- Add focus-visible style for search input accessibility
- Fix CSS specificity for active PR tab hover state
- Add viewChanges to actionsVisible search filter
- Wrap toggleSection in useCallback to avoid unnecessary re-renders
- Add windowsHide: true to getDefaultBranch subprocess
- Fix trailing slash handling in mockDaemon git action routing
- Add git methods to top-level client mock in tests
- Remove dead branchPicker.commitSuccess i18n key
- Remove dead .actionShortcut CSS class
- Show generation failure feedback in commit message placeholder
* fix(web-shell): address review feedback on branch picker PR (#7731)
- Add workspace trust checks to bound git branch routes
- Use workspace-scoped client in BranchPickerPopover (fixes wrong-workspace mutation)
- Add branch name validation and -- terminator to gitCreateBranch
- Filter refs/remotes/*/HEAD from branch listings
- Force LC_ALL=C for reflog parsing (non-English locale fix)
- Narrow 'could not resolve' error regex to avoid DNS false positives
- Add range validation to git log (reject path traversal)
- Fix commit+push error i18n (dedicated key instead of concatenation)
- Add i18n for BranchSelect component strings
- Fix commit tab ARIA attributes
- Add onBranchChanged callback to handlePush
- Add btw to mockDaemon isDaemonPath regex
- Add workspace_github_prs to visuals spec capabilities
- Add -- terminator regression test
- Remove docs/pr-assets/ from repo
* fix(web-shell): address review feedback on branch picker PR (#7731)
* fix(cli): reject dash-prefixed branch name with 400 in branch route (#7731)
* fix(web-shell): address review feedback on git branch picker (#7731)
Security:
- Clear GIT_DIR/GIT_WORK_TREE/GIT_COMMON_DIR/GIT_INDEX_FILE from git
subprocess env to prevent repository redirection
- Add strict mutation gate to all POST git branch routes
- Add generation guard to qualified write routes
- Fail closed on invalid ?cwd= in mutation routes (resolveContainedCwdOrFail)
- Reject wrong-typed startPoint, fetchOnly, rebase, and PR options with 400
Correctness:
- Filter remote symbolic refs (origin/HEAD) by %(symref) instead of /HEAD
name suffix, preserving valid branches like feature/HEAD
- Add git rev-parse --git-dir probe so non-git dirs get 404 instead of
empty available:true
- Push preserves existing upstream; only adds --set-upstream when unset,
resolving the remote from branch config or the sole configured remote
- git commit -a replaced with git add -A + git commit so untracked files
displayed in the UI are included
- Always pass --body to gh pr create to prevent interactive prompts
- getDefaultBranch returns null instead of fabricating origin/main
- Memoize workspaceByCwd client in BranchPickerPopover to fix infinite
render loop
- Move sessionId to a ref in GitDialog effects to prevent self-abort
- Bound commit-message prompt to fit /btw 4096-char limit
- Mark all platforms as unverified in PR template (no fabricated ✅)
- Guard PR auto-fill effect against wiping user edits on reconnect
Accessibility:
- Add tabIndex and onKeyDown to commit-mode tab span
- Add aria-label to BranchSelect trigger and search input
Cleanup:
- Remove dead CSS (.prInputSmall, .prSelect)
- Remove 9 unused i18n keys
- Add ^ to git log range validation regex
- Add busyAction guard to handlePush/handlePull
- Add unit tests for gitCommit, gitPull, and route input validation
* fix(web-shell): address review feedback on git branch picker PR (#7731)
- Change commit tab from <span> to <button> for keyboard accessibility
- Move setCommitMsg('') to success-only branches so the message is
preserved when push fails after a successful commit
- Add mutate middleware and generationGuard to PR creation route,
matching all other POST mutation routes
- Set genFailed when session resolution returns undefined so the user
sees the failure indicator instead of a silent empty textarea
- Make PR number nullable when URL regex does not match instead of
returning a misleading 0
- Add LC_ALL=C and LANG=C to gitEnv() so for-each-ref upstream track
parsing is locale-independent
- Validate setUpstream and force as booleans in handlePush, matching
the existing validation in handlePull
- Add missing workspaceCwd and available fields to test mocks
- Use stable data-web-shell-git-branch attribute in e2e selector
* fix(web-shell): address R5 review feedback on git branch picker PR (#7731)
- Classify git errors on stdout+stderr instead of err.message to fix
false-positive no_upstream on every push failure and dead
nothing_to_commit classifier
- Sanitize workspace paths and cap error message length in sendGitError
- Fix remote branch checkout to strip remote prefix so git DWIM creates
a local tracking branch instead of detaching HEAD
- Restore keyboard accessibility on composer branch chip (span → button)
- Trim startPoint in handleCreateBranch before forwarding to git
- Return bare branch name from getDefaultBranch (strip remote prefix)
- Fix i18n shortcut hint to show ⌘/Ctrl+Enter for cross-platform
- Update aria-label to reflect git management menu, not just changes
- Add available: true to mockDaemon gitDiff default payload
- Add regression tests: upstream preservation, sole remote resolution,
strengthened fetch-only with divergent remote commit
- Add aria-expanded assertion to sidebar picker test
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R7 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R8 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R9 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R10 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R11 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R12 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R13 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R14 review feedback on git branch picker PR (#7731)
* fix(web-shell): address R15 review feedback on git branch picker PR (#7731)
- Validate localName derived from remote-tracking ref to prevent option
injection (e.g. origin/-f → git checkout -f)
- Add gitCwd prop to BranchPickerPopover and pass it to all git SDK calls
so worktree sessions target the correct directory
- Add symlink-escape and non-existent-path tests for resolveContainedCwdOrFail
- Pin initial branch name in makeRepo() with git init -b master
- Add gitPull merge and rebase integration tests
* fix(web-shell): address git branch picker review feedback (#7731)
* fix(web-shell): address R6 review feedback on git branch picker PR (#7731)
* fix(web-shell): address review feedback on git branch picker PR (#7731)
- Strip GIT_CONFIG_GLOBAL/SYSTEM/NOSYSTEM in gitEnv to prevent
inherited config redirection (consistent with extension/github.ts)
- Pass gitCwd to workspaceGitBranches in GitDialog loadPrBranches
so worktree sessions fetch branches from the correct repository
- Add aria-expanded to collapsible branch section headers
- Add happy-path tests for PR create (201) and default-branch (200)
routes, including the null fallback to origin/main
* fix(cli): add sendGenerationClosedError to POST routes and cover untested branches (#7731)
* fix(web-shell): address review feedback on branch picker and PR creation (#7731)
- Refresh branch list after push/pull to avoid stale ahead/behind counts
- Add pre-flight check for unpushed branches before PR creation
- Fix base branch prefix stripping when branch list is unavailable
- Cap PR body file list at MAX_SUMMARY_CHARS to bound model prompt size
- Add qualified route tests: trust guard, input validation, cwd containment
* fix(web-shell): hoist MAX_SUMMARY_CHARS to module scope for PR body generation (#7731)
* fix(web-shell): address review feedback for git branch picker (#7731)
- Hoist onOpenCommit to useCallback to fix App.test.tsx prop stability test
- Keep commit tab visible after navigating away (startedInCommit ref)
- Add onClick handler to commit tab for navigation back to commit view
- Fix branch-prefix strip mangling local branch names containing '/'
- Update sessionIdRef after force-creating a stale session replacement
- Pin core.hooksPath in test makeRepo for reliable rollback tests
- Add test asserting --force-with-lease is used for force pushes
* fix(web-shell): target the worktree for sidebar commits and harden git actions (#7731)
Scope the sidebar commit dialog to the active session's worktree checkout
(matching the composer path) so linked-worktree sessions commit to the right
checkout, guard PR creation against a double-click race, and surface an error
when an invalid branch name is submitted. Adds focused coverage for the branch
picker action wiring and the git branch route validation paths.
* fix(web-shell): address review feedback for git branch picker (#7731)
---------
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
* feat(channels): dispatch GitHub notifications by reason
Route each GitHub notification by notification.reason into one of five
lanes, instead of dispatching every new comment regardless of trigger:
- mention: only dispatch comments that actually @ the bot (noise reduction)
- review_requested (PR): fetch PR meta via pulls.get and dispatch a
review-specific prompt, even with no new comments
- assign: fetch issue meta and dispatch a triage-specific prompt
- author/comment: aggregate the window's new comments into one check-and-
respond prompt
- other reasons: generic fallback (current behavior)
Add cursor dedup via dispatchedComments (by comment node_id) and
dispatchedNotifications (by notification id), surviving a
markNotificationsAsRead failure that leaves the cursor un-advanced.
Closes#7807
* fix(channels): mark review_requested/assign envelopes as mentioned
GroupGate defaults to requireMention: true, which silently drops
isMentioned:false envelopes as 'mention_required'. The review_requested
and assign lanes are explicit directed triggers — the bot was asked to
review or assigned — equivalent to a mention, so set isMentioned: true
so they pass the gate instead of being inert on the documented default
config.
Addresses review Critical on #7826.
* fix(channels): resolve github routing review comments
* fix(channels): dedupe github meta lane comments
* fix(channels): conditional assign framing for PR threads
The assign route already detected PR threads to use pulls.get, but the
trigger framing text always read 'assigned to this issue' even for PRs.
Make it conditional so PR assignments read 'assigned to this pull request'.
* fix(channels): dedup meta lane dispatch inputs
* fix(channels): simplify GitHub reason dispatch
* fix(channels): respect mention gate for github aggregate lane
* fix(channels): truncate aggregate comment bodies by code points
Match the code-point-aware truncation already used for meta-lane bodies
so a supplementary-plane emoji at the MAX_COMMENT_CHARS boundary is not
split into a lone surrogate.
* fix(channels): harden GitHub dispatch failures, event window, and framing (#7826)
- Classify deleted/transferred subjects (404/410) as terminal so a single
dead notification is logged and skipped instead of wedging the batch's
mark-read and cursor advance every poll.
- Widen the review_requested/assign event search to the newest ~100 events
by merging the preceding page when the last page is partial, instead of
inspecting only the last page (which can hold a single event).
- Move the aggregate lane's untrusted-data warning to the head of the prompt
text so it precedes the comment text it describes (metadata is appended
after text by ChannelBase).
- Add regression tests: permanent-failure two-poll advance, terminal 404
no-retry, multi-page event search, prompt caps, and the no-actor guard.
* fix(channels): drop lastReadAt filter in findMetaTrigger, add review coverage (#7826)
* fix(github): keep aggregate and meta windows bounded
* fix(channels): apply windowSince lower bound in findMetaTrigger (#7826)
* fix(channels): bound retry wedge, compute aggregate isMentioned, fix pairing pre-filter (#7826)
* fix(github): record dispatch before handler
* fix(github): persist skipped notifications
* fix(github): close dispatch retry loss cases
* test(github): cover cursor trim and meta floor validation
* fix(github): simplify notification reason dispatch
* fix(github): preserve batched dispatch comments
* fix(github): restore direct event dedup
* fix(github): preserve directed mention context
* fix(github): keep review fixes scoped
* fix(github): preserve delayed direct triggers
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
* fix(review): recover the resolved effort when --effort is not re-threaded
The capture commands (capture-local, plan-diff, fetch-pr) record the review
effort as plan.effort, which every downstream consumer — roster, check-coverage,
compose-review — reads. They learn it from --effort <level>, and the skill asks
the orchestrator to pass the level parse-args resolved. But that is a value the
model must copy from one file into a flag, and it does not reliably happen: a
`/review --effort medium` local run had the orchestrator omit the flag, so
plan.effort was absent and the roster safe-expanded to the FULL set — the user
asked for the reduced medium roster and silently got every agent (6a/6b/6c
included).
Close the gap deterministically. A new resolveEffort() prefers an explicit
--effort, and otherwise reads the level parse-args already wrote to its
conventional report (.qwen/tmp/qwen-review-parse-args.json). When neither is
available it returns undefined and the roster fail-safes to the full set exactly
as before, so a missing report never reduces coverage, and a malformed level is
ignored rather than trusted.
* refactor(review): dedupe effort resolution per review feedback
- Share one EFFORT_LEVELS set: export it from parse-args and import it in
effort.ts, so a new level cannot be added to one set but not the other.
- Collapse the three identical effort-spread IIFEs into planEffortField().
- Isolate CWD in plan-diff.test.ts (matching capture-local.test.ts) so
resolveEffort's CWD-relative report read cannot pick up a stale file and
fail the "omits effort" case.
* test(review): pin PARSE_ARGS_REPORT value and plan-diff effort fallback (#7855)
* test(review): dedupe seed helper, cover fetch-pr effort, trace resolution (#7855)
* fix(review): spread planEffortField last for consistent effort precedence (#7855)
---------
Co-authored-by: verify <verify@local>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
* fix(web-shell): report intended workspace to host when starting a new chat
Clearing a session leaves connection.workspaceCwd pointing at the previous session's workspace. The onSessionIdChange notification read that stale value, so starting a new chat in workspace A routed the host back to the old workspace (e.g. one with a running task) and the composer showed the wrong workspace. With no active session, report the workspace picked for the next session instead.
* fix(web-shell): reuse activeWorkspaceCwd for the no-session host report (#7910)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
`isApproveOutcome` is documented as the single source of truth for "the
user said yes", shared by the CLI scheduler and the ACP Session so the two
cannot drift. It listed every proceed_* outcome except
`proceed_always_server` and `proceed_always_tool`.
Those two are deprecated but live. web-shell's ToolApproval sends them,
and `background-tasks.ts`, `telemetry/tool-call-decision.ts`, the ACP
Session and `agent.ts` all treat them as approvals. This function was the
only place that did not.
Both consumers use it for the same thing: in AUTO mode, when a call fell
back to a manual prompt because denialTracking was armed, an approval
clears the counters so later calls return to classifier flow. A user who
approved with the "always allow for this server" or "for this tool" button
did not clear them, so the session kept prompting after the user had
granted the broadest permission of all.
The test now enumerates the proceed_* values from the enum instead of
hand-listing them -- the hand-written list is what drifted, while claiming
to cover "every proceed_* outcome".
Quote tracking was gated on `substitutionDepth === 0`, so quotes inside a
`$(...)` body were invisible to the parser. A `)` inside those quotes then
closed the substitution early, and the body's own closing quote -- now
seen at depth 0 -- flipped the parser into "in quote" state, which
swallowed every separator to the end of the line.
splitCommands(`echo $(echo ')') ; rm -rf /tmp/pwned`)
-> ["echo $(echo ')') ; rm -rf /tmp/pwned"] one segment
getCommandRoots(same)
-> ["echo"] rm is gone
The trailing command did not merely stay joined to the first, it vanished
from the roots. shell.ts uses these segments to decide which sub-commands
need confirmation, and to locate the git commit / gh pr create segment for
attribution, which silently no-ops on a mis-split.
Gating the closing paren on the surrounding quote state is not enough,
because `"$(...)"` puts a double quote around the whole substitution and
that quote belongs to the outer command. Save the enclosing quote state on
`$(` and restore it on the matching `)`, so each body is quoted
independently of its surroundings.
splitCommands had no direct unit tests; this adds them, including the
nesting and quoting shapes that must keep behaving as they do.
* fix(core): short-circuit the flush wait when the output stream cannot flush
Review follow-up on the settle flush wait:
- A destroyed stream (autoDestroy after an earlier EIO/ENOSPC write
error) or an already-finished one emits neither 'finish' nor 'error',
and .end() on it is a silent no-op — the settle stalled for the full
10s timeout with nothing left to flush, and an abortAll() landing in
that window (/clear, shutdown) would mark the entry cancelled.
Short-circuit before arming the timer. writableFinished, not
writableEnded: the latter is already true mid-flush.
- Hoist clearTimeout into runOnce so the synchronous-throw path no
longer leaves the timer armed to log a misleading flush-timeout
warning for a shell that settled long ago.
- Drop the vacuous duplicate-'finish' assertion (once semantics already
drained the handler; the guard is pinned by the 'error' emit and the
timeout test's late finish) and cover both short-circuit states with
deferred-stream tests asserting the immediate transition and that
.end() is never called.
* test(core): pin the mid-flush wait and the throw-path timer disarm
Closes the two coverage gaps from the verification review on #7905 —
both mutations previously survived the full suite:
- guard mutated to include writableEnded (short-circuit mid-flush, the
exact truncation the flush wait prevents) — killed by a deferred
stream with { writableEnded: true, writableFinished: false }
asserting the transition still waits for 'finish';
- hoisted clearTimeout dropped from runOnce — killed by a throwing
end() plus fake timers asserting no flush-timeout warning fires
after the timeout elapses.
---------
Co-authored-by: ComplexSimply <rudy.arrowsong@gmail.com>
* fix(safe-mode): preserve caller-supplied top-tier MCP servers
Safe mode is meant to distrust LOCAL/ambient state (settings.json,
extensions, project .mcp.json) so a user can isolate which local
customization is misbehaving. It was also unconditionally dropping
topTierMcpServers -- the caller-supplied servers from an ACP
session/new's mcpServers field or --mcp-config -- which are an
explicit, per-invocation argument, not ambient local state.
The real gate turned out to be in packages/core/src/config/config.ts's
Config.getMcpServers() (the accessor mcp-client-manager.ts actually
reads for discovery), not just the mcpServers assembly in
loadCliConfig -- fixed both so the raw field and the accessor agree.
Also guarded the hot-reload path (hot-reload.ts) with the same
bare/safe-mode check, so a live settings.json edit can't smuggle local
servers into an already-running bare/safe-mode session.
Fixes#7819
* docs: clarify safe-mode's MCP servers note distinguishes local vs caller-supplied
Per CONTRIBUTING.md guideline 5 (docs for user-facing changes).
* fix(safe-mode): apply allowedMcpServers to top-tier servers, skip pendingMcpServers I/O under safe mode
Address Copilot's review of PR #7827:
- getMcpServers() now runs the safe-mode top-tier map through the same
allowedMcpServers filter as the non-safe-mode path -- safe mode is
not an exemption from a session's own --allowed-mcp-server-names
upper bound (High severity finding, confirmed real).
- Re-added safeMode to pendingMcpServers' skip condition in
loadCliConfig. Functionally a no-op either way (top-tier servers are
never gated, #4615), but skips getMcpApprovals' local file read
entirely under safe mode instead of doing a no-op read -- safe mode
shouldn't touch local/ambient state at all, not even harmlessly
(Medium severity finding).
* fix(safe-mode): actually run MCP discovery for surviving top-tier servers
Config.getMcpServers() reporting a top-tier server as configured isn't
enough on its own -- something has to actually connect to it and
register its tools with the model. That's a separate gate in
initialize(): startMcpDiscoveryInBackground() was skipped outright
whenever isSafeMode() was true, written back when getMcpServers()
always returned {} under safe mode (so skipping discovery was a
harmless no-op). Left unpatched after the earlier fix, a caller-supplied
top-tier server survives getMcpServers() but is never actually
discovered/connected -- confirmed live against a real ACP session
before this commit: the agent reported the tool as "not configured"
even though Config.getMcpServers() already returned it.
Found by actually running the fix end-to-end against a live ACP
session (qwen --acp --safe-mode + a real stdio MCP fixture server)
instead of relying on unit tests of Config in isolation. After this
commit the same live session correctly discovers and calls the
caller-supplied tool.
Checks getMcpServers() (not topTierMcpServers directly) so the
allowedMcpServers filter still applies -- no discovery is kicked off
if the only top-tier server present is filtered out.
* fix(safe-mode): also run MCP discovery for surviving bare-mode top-tier servers
The discovery-kickoff gate in Config.initialize() special-cased safe
mode (skip only when there's nothing to discover) but left bare mode's
half of the same guard unconditional (!this.getBareMode()), even
though loadCliConfig feeds top-tier MCP servers into bare mode's
mcpServers assembly exactly the way it does safe mode's
topTierMcpServers field. A bare-mode session with a caller-supplied
server (qwen --bare --mcp-config, or ACP session/new under bare mode)
had that server reported as configured by getMcpServers() but never
actually connected/discovered — the same stranded-server regression
already fixed for safe mode in this PR, just the bare-mode twin of it.
Found by an automated review pass on PR #7827 after the safe-mode fix
had already landed. Added the same three-case regression coverage
(present / nothing supplied / filtered out by allowedMcpServers)
mirroring the existing safe-mode tests, using mcpServers (not
topTierMcpServers) since bare mode's "local sources dropped" guarantee
lives entirely in the CLI-layer assembly, not a core-level short-circuit.
* refactor(safe-mode): simplify the bare/safe discovery-gate condition
(!bare || has) && (!safe || has) reduces by distributivity to
!(bare || safe) || has, so factor the has-servers check into a single
hasMcpServers computed once, instead of calling getMcpServers() (which
allocates and filters) twice.
Suggested by an automated review pass on PR #7827, commit 25ddf8b.
No behavior change — same 23 discovery-gate tests pass unmodified.
* fix(safe-mode): stop reading settings.mcp.allowed/excluded under safe mode
allowedMcpServers/excludedMcpServers assembly in loadCliConfig() only
guarded the settings-sourced branch with `!bareMode`, missing `!safeMode`
— so a local settings.json mcp.allowed/excluded list (LOCAL/ambient state,
same category as settings.mcpServers itself, which safe mode already
drops) was still read under safe mode. Combined with getMcpServers()'s own
allowedMcpServers filter (added earlier in this PR for the
--allowed-mcp-server-names case), a settings.json mcp.allowed list
narrower than the caller's own top-tier servers would silently filter
them back out — defeating the guarantee this PR exists to provide, via
the filter's source rather than the mcpServers map directly.
The argv.allowedMcpServerNames branch is unaffected: that's an explicit
per-invocation argument, not local state, so it still applies under safe
mode same as topTierMcpServers itself.
Found by an automated review pass (doudouOUC, CHANGES_REQUESTED) on PR
#7827. Regression test confirmed red before the fix (session-supplied
server silently filtered out) and green after. Full targeted vitest run
(packages/cli config/: 1018 passed, same 3 pre-existing Windows-only
extension-file-watcher failures as before, unrelated), tsc --noEmit,
eslint, prettier --check all clean.
* fix(safe-mode): stop reading settings.mcp.allowed/excluded on hot-reload too
Same class of bug as the previous commit's loadCliConfig fix, found by an
automated review pass on the SAME PR: recomputeMcpGating (hot-reload.ts)
reads settings.merged.mcp.allowed/excluded unconditionally, with no
bare/safe guard of its own. registerMcpHotReload's existing bare/safe
guard only covered the servers map (`next`), not the admission lists
computed right after it — so a live settings.json edit narrowing
mcp.allowed during an already-running safe/bare session would flow
straight into setAllowedMcpServers and silently filter the caller's
top-tier server out of getMcpServers() mid-session. Same stranded-server
outcome as the boot-time bug, reached through the gating list's SOURCE
instead of the mcpServers map.
Fix: under bare/safe mode, skip recomputeMcpGating entirely and build the
gating directly from only the CLI --allowed-mcp-server-names bound
(explicit, per-invocation, not local state — same treatment as
topTierMcpServers itself); excluded/pending are irrelevant once nothing
but the never-gated top-tier servers can be present.
Regression tests (safe mode + bare mode) confirmed red before the fix
(setAllowedMcpServers called with the settings-sourced list) and green
after (called with the CLI bound, undefined here). Full targeted vitest
run (packages/cli config/: 1020 passed, same 3 pre-existing Windows-only
extension-file-watcher failures as before this PR touched anything,
unrelated), tsc --noEmit, eslint, prettier --check all clean.
* fix(safe-mode): stop reading settings.mcp.allowed/excluded on ACP reload too
Third instance of the same bug class found by an automated review pass on
this PR: reloadWorkspaceMcpDiscovery (packages/cli/src/acp-integration/
acpAgent.ts) — the ACP control-endpoint reload path (workspaceMcpReload),
distinct from registerMcpHotReload's settings-file-watcher path fixed in
the previous commit — called assembleMcpServers(settings.merged.mcpServers,
...) and recomputeMcpGating(settings, ...) unconditionally, per live Config
in liveConfigs, with no bare/safe guard. A workspaceMcpReload request
against an already-running safe/bare session would fold local
mcpServers/mcp.allowed/excluded back in, silently stranding or filtering
the caller's own top-tier server mid-session — same outcome as the two
prior fixes, reached through a third independent reload path.
Fix: per-config (liveConfigs holds a Set of potentially differently-moded
Configs — the base config, active session configs, and the discovery
config), skip assembleMcpServers/recomputeMcpGating under bare/safe mode
and build servers/gating directly from that config's own
getTopTierMcpServers()/getCliAllowedMcpServerNames() — same treatment as
the other two fixes.
Regression test (packages/cli/src/acp-integration/acpAgent.test.ts)
confirmed red before the fix (settings-sourced 'local' server leaked
into reinitializeMcpServers alongside the caller's 'probe') and green
after. Also added getBareMode/isSafeMode mocks (defaulting false) to the
two pre-existing Config-shaped mocks in this describe block that didn't
have them — reloadWorkspaceMcpDiscovery now calls these unconditionally
per config, which would otherwise throw "not a function" against any
mock missing them, even for a normal-mode test. Full targeted vitest run
(packages/cli/src/acp-integration/acpAgent.test.ts: 318 passed), tsc
--noEmit, eslint, prettier --check all clean.
* test(safe-mode): add bare-mode counterpart for the workspaceMcpReload guard
Suggested by an automated review pass on PR #7827: the previous commit's
regression test for reloadWorkspaceMcpDiscovery only exercised
isSafeMode: true, leaving the bare-mode half of
config.getBareMode() || config.isSafeMode() unverified at this layer —
unlike the hot-reload.ts tests, which already cover both modes for both
the servers map and the admission lists. A future change narrowing that
guard to isSafeMode() only would go undetected here.
Confirmed red before the fix (temporarily reverted acpAgent.ts to the
prior commit): settings-sourced 'local' leaked into reinitializeMcpServers
alongside the caller's 'probe', same as the safe-mode case. Green with
the fix restored. Full targeted vitest run (acp-integration/acpAgent.test.ts:
319 passed), tsc --noEmit, eslint, prettier --check all clean.
* fix(cli): remove redundant "Read file" prefix from @mention tool card
The @mention file-read tool card set its description to
"Read file <name>", which duplicated the display name ("Read")
when rendered as "{displayName} {description}", producing
"Read Read file README.md".
Change the description to "@<name>" — matching the @mention
syntax the user typed, and consistent with how normal read_file
tool calls show just the filename in their description.
* test(cli): assert @mention tool card description format (#7902)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
* fix(cli): add polling fallback for git branch name display (#7828)
fs.watch on .git/logs/HEAD is unreliable on NFS, FUSE, Docker overlay,
and some Linux filesystems — events can be silently dropped with no
recovery path, leaving the footer branch name stale indefinitely.
Added a 5-second polling fallback in useGitBranchName that calls
resolveBranchName and updates the state only when the value changes.
The timer is unref'd so it doesn't keep the process alive. The
existing fs.watch mechanism is preserved for immediate updates when
it works correctly.
* test(cli): cover the git branch polling fallback (#7830)
* refactor(cli): use idiomatic timer.unref?.() in branch poller (#7830)
* fix(cli): order concurrent branch refreshes by generation (#7830)
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>