* feat(cli): skill review dialog — inline preview, open-in-editor, turn-off option
The auto-skill review dialog now shows the staged SKILL.md inline
(sanitized, bounded reads, wrap-aware height cap), opens it in the
configured editor without advancing (with watcher-based preview refresh
so non-blocking GUI editors work), and offers a visible last option to
turn the feature off — effective immediately in-session, persisted at
workspace scope, non-destructive to the pending batch. Bulk options
render only while at least two skills remain. Re-enabling auto-skill
from /memory can resurface a batch put aside by turn-off.
* test(integration): render harness + capture scenarios for the skill review dialog
Browser-free harness that renders the production dialog from source via
an ESM loader hook; its before mode renders the globally installed qwen
(no local fixture — the baseline is what actually shipped, or a loud
failure). Terminal-capture scenarios produce the PR's before/after
screenshots.
* fix(cli): address review findings on the skill review dialog
- Sanitize the model-generated name and description in the dialog header,
same as the preview body — an escape sequence in the frontmatter must not
reach the terminal through the header fields.
- Clamp the preview width to the dialog container cap (min(columns-4, 100),
the same clamp DiffDialog uses) instead of the raw terminal width, which
broke the wrapped-row accounting on terminals wider than ~106 columns.
- Catch settings persistence failures in the turn-off option: surface the
error in the dialog and leave the feature untouched instead of letting the
throw escape the keypress handler.
- Extract the auto-open gate into shouldAutoOpenSkillReview and cover it
with a truth table (turn-off, /memory overlap, re-enable, Esc-dismiss).
- Cover the MemoryDialog auto-skill ON->OFF toggle direction.
- Release the capture harness temp dir with try/finally.
* fix(cli): guard the preview watcher against async errors and event bursts
An FSWatcher 'error' event after attach had no listener, so Node raised
it as an uncaught exception and the global handler exited the CLI.
Consume it and drop the watcher; the blocking-editor reload still works.
Also debounce the watch callback (300ms, same as SettingsWatcher): a
single editor save fires several raw events, and each one re-read the
file and re-attached the watcher.
* test(cli): drop white-box watcher tests, keep the end-to-end refresh test
The prototype-spy scaffolding tested implementation details (listener
registration, synthetic event bursts) and leaned on vite-node interop
quirks. The existing on-disk refresh test already exercises the watcher
path, debounce included.
* fix(cli): sanitize action errors, log preview read failures, cover key guards
- Render actionError through sanitizeMultilineForDisplay: error messages
can embed the staged path, whose basename derives from the
model-generated skill name.
- Log the underlying cause when the preview read fails; all failures
render the same 'Preview unavailable' otherwise.
- Cover Ctrl+O/Cmd+O inertness and Esc dismissal with tests.
- Document that getAutoSkillEnabled() also gates on bare/safe mode.
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The WeCom intelligent robot channel (added in #6436) has its own guide and _meta.ts entry, but the channels overview was never updated. Add WeCom to the platform list, quick-start guide links, the `type` option and `token` exclusion note, new `botId`/`secret` option rows, the media-support note, and the slash-command channel enumeration.
Co-authored-by: Claude <noreply@anthropic.com>
* fix(web-shell): rename scheduled tasks "查看历史" to "查看对话"
* feat(serve): bind cron_create durable tasks to dedicated sessions via keepalive
The cron_create tool (core layer) writes durable tasks to disk without a
sessionId because it has no access to the session bridge. The keepalive
loop runs in the daemon process where the bridge IS available, so it
retroactively binds unbound tasks to dedicated sessions — the same flow
POST /scheduled-tasks uses for UI-created tasks. Each unbound task gets:
spawnOrAttach(sessionScope:'thread'), named ⏰ prompt, sessionId written
back to disk. This makes chat-created tasks show "查看对话" with a clock
icon in the session list, matching the UI's "新建定时任务".
* feat(serve): watch tasks file for immediate binding of new cron_create tasks
The keepalive interval is 2-5 minutes, so a chat-created task could wait
that long before being bound to a dedicated session — showing no "查看对话"
link until the next tick. Adding a file watcher (same directory-watch +
debounce pattern the scheduler uses) triggers an immediate tick when
cron_create writes to disk, so the task is bound within ~500ms.
* feat(serve): bind cron_create tasks to current session + ⏰ rename via keepalive
Switch from creating a separate dedicated session to binding the task to
the current chat session (so the first message is already in the
transcript). The keepalive then renames that session to ⏰ prompt — the
core layer can't rename sessions (no bridge access), but the daemon
process can. A Set tracks renamed sessions to avoid repeated
updateMetadata calls. Unbound tasks (legacy/CLI) still get new sessions
via the existing bind path.
* fix(core): keep createDurable() tasks unbound by default
Reverts the auto-binding of durable tasks to the current session in
createDurable(). Binding to a specific session means only that session
can fire the task (#shouldFireDurable), but non-daemon paths (TUI, ACP,
headless) have no keepalive to rehydrate the session after exit — making
tool-created durable tasks go dormant.
The daemon keepalive (bindAndNameSessions) already handles binding
unbound tasks to dedicated sessions with ⏰ naming, so daemon-mode
tasks get the same UX without the regression.
* fix(serve): roll back orphan sessions in keepalive binding + add tests
When bindAndNameSessions spawns a dedicated session for an unbound task
but the subsequent updateCronTasks write fails (or the task was deleted
between read and write), the spawned session was left behind with no
owning task — the next tick would see the task still unbound (or spawn
more orphans). Add rollback: closeSession + removeSession on failure,
matching the POST /scheduled-tasks rollback pattern.
Also add positive test coverage for the new binding paths:
- unbound task → spawn + name + write sessionId to disk
- bound task without ⏰ prefix → named exactly once (renamed Set dedup)
- task vanishes before write → spawned session is rolled back
* fix(serve): add timeout to spawnOrAttach in keepalive binding + test hardening
BZ-D: spawnOrAttach in bindAndNameSessions had no timeout boundary — a
hung spawn would keep running=true and stall all subsequent ticks,
stopping heartbeats/revives for every scheduled-task session. Wrap with
withTimeout (configurable via spawnTimeoutMs, default 30s) and attach a
background handler to clean up late-resolved orphans.
Also generalized withTimeout error messages to include the operation
name, and made spawn timeout configurable for tests.
Test improvements (GPT-5 review suggestions):
- Assert spawnOrAttach payload (workspaceCwd + sessionScope: thread)
- Verify SessionService.removeSession called during rollback
- Regression test: createDurable stays unbound after enableDurable
- Hung-spawn test: tick completes despite non-abortable spawn hang
* fix(serve): keepalive hardening + i18n sync (review suggestions)
- i18n: sync English 'View history' → 'View conversation' to match
Chinese '查看对话'
- Prune renamed Set alongside reviveState when tasks are removed
- fs.watch: clarify null filename handling for Linux (treat as match)
- updateCronTasks: skip .map() when task not found (no-op optimization)
- Add tests: disabled unbound exclusion, naming failure resilience
* perf(cli): defer startup prefetch tasks
* fix(cli): await IDE for prompt-interactive startup
* perf(cli): defer interactive telemetry startup
* test(cli): add missing assertions and Zed/ACP path coverage for startup prefetch
Address three test coverage gaps identified during code review:
- Assert mockStartEarlyStartupPrefetches in both kitty protocol tests
(C1: API preconnect call was wired but never verified)
- Add Zed/ACP integration test verifying deferIdeConnection is false
when getExperimentalZedIntegration returns true (C2: Zed path was
entirely untested)
- Assert mockStartBackgroundHousekeeping in startup-prefetch test
(C3: unconditional housekeeping dispatch was never verified)
* docs: move startup prefetch design doc to performance subdirectory
* docs: translate startup prefetch design doc to English
* fix(cli): address startup prefetch review comments
Tighten the startup prefetch follow-up fixes from review while keeping
prompt-interactive telemetry on the fast interactive startup path.
- Preserve Error objects when deferred startup tasks fail
- Remove the unbalanced api_preconnect profiler lifecycle event
- Guard background housekeeping so it only runs for interactive configs
- Document and test prompt-interactive telemetry deferral semantics
* fix(cli): initialize telemetry for prompt-interactive prompts
Ensure sessions launched with an initial interactive prompt have
telemetry ready before the auto-submitted first request runs.
- Exclude prompt-interactive startup from telemetry deferral
- Pass a post-render telemetry option through interactive UI startup
- Skip duplicate post-render telemetry startup for initial prompts
- Update tests to cover the first-prompt telemetry guarantee
Note: Plain interactive TUI startup still defers telemetry post-render.
* fix(cli): preserve startup first-request guarantees
Keep deferred startup work from weakening first-request behavior in
interactive sessions that submit prompts automatically or remotely.
- Store telemetry deferral on Config and reuse that decision at render time
- Keep IDE startup awaited for prompt-interactive and input-file sessions
- Add a timeout for deferred IDE connection failures
- Cover ordinary interactive telemetry deferral and IDE startup edge cases
* fix(cli): make post-render IDE connection opt-in
Default startInteractiveUI to the already-connected IDE path so future
callers do not accidentally connect twice when initializeApp used its
eager default.
- Change the post-render IDE connection default to false
- Update startInteractiveUI tests to assert the safer default
* perf(cli): surface deferred IDE connection status
Make ordinary interactive IDE startup visible while preserving the
post-render prefetch path and first-paint performance tradeoff.
- Emit deferred IDE connection lifecycle events for connecting, success,
and failure states
- Surface IDE startup status in the TUI footer without blocking input
- Log late underlying IDE failures after timeout for better diagnostics
- Document telemetry deferral tradeoffs and add startup lifecycle tests
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* feat(channels): add natural channel memory intents
* fix(channels): add explicit guard and exhaustiveness check for clear_confirm intent
The clear_confirm path was handled as implicit fall-through at the bottom
of handleChannelMemoryIntent. If a new intent kind were added to the
ChannelMemoryIntent union, it would silently execute clearChannelMemory
without user confirmation — a data-loss risk.
Add explicit if (intent.kind === 'clear_confirm') guard and a
const _exhaustive: never assertion so TypeScript flags any unhandled
kinds at compile time.
* fix(channels): close session leak in classifier and fix regex separator
- BridgeChannelMemoryIntentClassifier now wraps prompt() in try/finally
to always call cancelSession(), preventing daemon session leaks on
every classifier invocation. Cleanup errors are caught so they cannot
mask a successful classification result.
- Add missing optional punctuation separator to the 以后记住 regex
pattern for consistency with other Chinese remember patterns.
* fix(channels): enforce pending clear state for channel memory confirmation
The clear_confirm intent executed clearChannelMemory directly without
verifying a prior clear_request was issued for the same chat. Any
authorized user could clear any chat's memory by sending the confirmation
phrase standalone, bypassing the two-step flow.
Add a per-target pending clear map (chatId + threadId, 60s TTL) that
is set during clear_request and verified+consumed during clear_confirm.
Standalone confirmation phrases now get rejected with a prompt to
issue the clear request first.
* fix(channels): include senderId in pendingClears key to prevent cross-user confirmation
User A could initiate clear_request in a group chat and User B could
confirm it, since the pending key only included chatId+threadId.
Add senderId to the key so only the user who initiated the clear
can confirm it.
* fix(channels): harden memory intent review fixes
* fix(channels): cover memory clear sender guard
* fix(channels): block group memory mutations
* fix(channels): avoid ambiguous memory saves
* test(channels): cover memory classifier cleanup
* test(channels): cover memory clear expiry
* fix(channels): restore channel memory slash aliases
* test(channels): cover memory intent edge cases
* fix(web-shell): remove meaningless "current" badge from split-view panes
In the split view every pane is an equal, independently interactive session (its own DaemonSessionProvider, SSE, transcript, and approvals), so tagging one pane as the workspace's "current" session carried no operational meaning. It only leaked a single-view concept into a peer-of-equals layout and reliably prompted "what is this?" questions from users. Panes are already identified by their titles.
Drop the isCurrent badge and its border highlight from ChatPane, and stop passing isCurrent from SplitView. The sidebar and session overview keep their "current" indicators, which are legitimate "you are here" navigation. currentSessionId is retained only to seed the initial pane.
* fix(web-shell): clear the split-view composer on send, not at turn end
A split-view pane kept the just-sent text sitting in its composer until the whole turn finished. handleSubmit committed the draft on the sendPrompt promise resolving, but that promise resolves via waitForAcceptedPromptCompletion (turn end), not at admission. Switch to the onAdmitted hook so the composer clears the moment the daemon accepts the prompt, matching the main view. A prompt rejected before admission still preserves the draft and surfaces the error.
* fix(web-shell): pass commitAccepted directly as onAdmitted; cover admit-then-fail
Review follow-up:
- commitAccepted is already `() => void`, so pass it directly as the onAdmitted option instead of wrapping it in a redundant `() => commitAccepted?.()` closure.
- Add a test for the turn failing after admission: the draft stays cleared (no second commit) and the error is still surfaced to onError.
* fix(core): strip system-reminder pollution from session title and recap prompts
The `filterToDialog` function in sessionTitle.ts and sessionRecap.ts
was including system-reminder blocks (skills list, CLAUDE.md, MCP
announcements) in the prompt sent to the title/recap model. When the
user's first message was short, `flattenToTail` would reach back into
these injected entries, causing titles like "resolve-cr-comments" instead
of reflecting the actual conversation.
- Skip startup prelude entries via `getStartupContextLength`
- Strip `<system-reminder>` blocks from text parts
- Add shared `stripSystemReminderBlocks` helper in environmentContext.ts
- Add regression tests for both sessionTitle and sessionRecap
Closes#6419
* fix(core): preserve mixed reminder prompt turns
* fix(core): support large text range reads
Allow text reads to stream bounded line ranges for files larger than the previous 10MB guard, while preserving media size limits and forwarding cancellation through read_file/read_many_files/ACP paths.
Refs #6403
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): address large text review feedback
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): propagate abort signals in text reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): validate streamed utf8 reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): handle disabled line truncation for large reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): use kebab-case for text range reader
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): preserve artifact size errors for large sources
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): address large text review follow-up
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): allow default large text reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): honor text read byte caps
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): clarify invalid utf8 range read errors
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): prevent truncated full large reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): preserve unbounded line-zero reads
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): forward artifact read cancellation
Pass artifact execution abort signals into source file reads and preserve cancellation semantics when the read is aborted.
Add regression coverage for unbounded large UTF-8 range reads and offsets beyond EOF.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): address file read review feedback
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(core): preserve large text mutation reads
Allow default unbounded readTextFile calls to keep reading full large text files so mutation tools can prepare complete snapshots after a prior ranged read.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Split-view panes now render a compact streaming status: the spinner, elapsed time, token count, and cancel hint stay, but the rotating "witty" loading phrase is suppressed. StreamingStatus gains a showPhrase prop (default true, so the main chat is unchanged); when false it also skips the phrase-rotation timer so each pane avoids a needless interval.
Place the Qwen brand mark to the left of the sidebar's New chat button.
The artwork is the same SVG used for the browser-tab favicon (and the
QwenLM GitHub avatar), inlined rather than hot-linked because the Web
Shell CSP is `img-src 'self' data: blob:`, which blocks remote images.
When the sidebar is collapsed there is no room beside the compact
button, so the mark is hidden and only the New chat button remains.
* fix(web-shell): keep split-view session list fresh and preserve panes across view switches
The in-window split view's "add pane" picker read a stale session snapshot —
`useSessions` only fetches on mount — so sessions created after entering the
split never appeared. And switching away from the split and back cleared the
panes, because the live pane set lived in local state that died on unmount while
the seed it re-mounted from was never updated (and the no-arg "Open Split View"
button reset it to empty).
- Reload the picker list when it opens and when the parent's session-list reload
token changes, so it never offers a removed session or misses a new one.
- Mirror the live pane set up to the app via onPanesChange so it survives
SplitView unmounting; restore it (instead of reseeding empty) when the split is
reopened without an explicit selection.
* test(web-shell): cover split-view refresh/restore per review; coalesce token reloads
Addresses review feedback on #6418:
- SplitView: skip a token-driven reload while one is already in flight, so a
burst of session-list changes (bulk create/delete) doesn't fire a redundant
concurrent round-trip per bump (matches the sidebar's poll guard).
- SplitView test: the freshness test now proves the picker re-renders with the
refreshed list — a session appearing only after reload shows up — not just
that reload() was called.
- App test: cover the openSplitView preserve/restore path end-to-end — a reported
pane set survives leaving the split and is restored on reopen.
* fix(web-shell): reload split picker on every token bump (drop in-flight guard)
The in-flight guard added in the previous commit could drop a session-list
reload token that arrives while a reload is still running: the effect has
already run for that token value, and clearing the in-flight flag in `finally`
doesn't re-run it, so the picker could stay stale after burst create/delete/
rename activity — and the split has no polling fallback to recover.
Reload on every distinct token bump instead. `useDaemonResource` serializes
responses via its sequence counter (last write wins), so overlapping reloads are
correct, and the token is bumped only on discrete session-change events — an
occasional redundant fetch is far cheaper than a lost refresh.
* test(web-shell): cover openSplitView explicit-selection branch (dedupe + cap)
Per review: the restore branch of openSplitView was covered but the
explicit-selection branch (dedupe + MAX_SPLIT_PANES cap, replacing any prior
set) was only exercised, not asserted. Add a `?split=` URL test with duplicate
and over-cap ids that asserts the split seeds exactly the deduped, capped
selection.
When a skill is invoked multiple times in a session, each invocation
previously appended the full SKILL.md body content to the conversation
history as a new tool result, wasting context tokens.
Add an isSkillLoaded callback to SkillToolInvocation that checks
loadedSkillNames before building the full content. On re-invocation,
return a short confirmation message instead of the full body. The
check runs after successful skill load (so disabled/not-found paths
are unaffected) but before content construction, hooks registration,
and allowedTools application (which are idempotent and already applied
on first load).
Fixes#6427
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(core): add tools.visible config for selective deferred-tool visibility
* fix(cli): wire tools.visible from settings.json into Config
Add settingsSchema entry for tools.visible and plumb it through loadCliConfig into ConfigParameters.visibleTools. Without this the core-level visibleTools support was unreachable from settings.json.
Adds 3 CLI-level tests: visibleTools passthrough, empty default, safe-mode suppression.
Fixes#6368
* fix(core): exclude visibleTools from tool_search candidates and reveal path
Add visibleTools gate to collectCandidates() and loadAndReturnSchemas in tool-search.ts to prevent KV-cache invalidation when tool_search is invoked for a visible-deferred tool.
Without this, a tool listed in tools.visible would still appear as a keyword-search candidate and select: would still trigger revealDeferredTool + setTools, defeating the purpose of promoting it to first-class visibility.
3 tests: keyword-search exclusion, select: no-reveal/no-setTools, and select: still works for non-visible deferred tools.
* fix(cli): include visibleTools in /context per-tool token breakdown
Add config.getVisibleTools().has(tool.name) gate to the deferred-tool skip condition in collectContextData. Without this, visible-deferred tools appear in the headline total (via getFunctionDeclarations()) but are excluded from the per-tool breakdown, causing the sum to mismatch.
1 test: visibleTools included in breakdown despite deferred+unrevealed.
* fix: address all 7 review suggestions for tools.visible
1. settingsSchema: user-facing description instead of internal jargon
2. config.ts: use normalizeToolNameList (generic name) for both disabled and visible
3. config.test.ts: add bare-mode exclusion test
4. tool-registry.test.ts: disabledTools > visibleTools priority test
5. tool-registry.ts: update JSDoc for getDeferredToolSummary
6. tool-search.test.ts: mixed select: visible+non-visible test
7. tool-registry.test.ts: visible survives clearRevealedDeferredTools
* chore: regenerate settings.schema.json after description update
CI check detected that settings.schema.json was out of sync with settingsSchema.ts after the description was changed in commit 73e879882.
* fix: address wenshao review — extract isDeferredAndHidden, clean up abstractions
1. Remove normalizeToolNameList (empty wrapper, violates AGENTS.md no-abstraction rule)
2. Extract ToolRegistry.isDeferredAndHidden() — 5 call sites reduced to 1 predicate source
3. Add dirty-input test for tools.visible (whitespace, duplicates, empty strings)
4. Add MergeStrategy.UNION test for tools.visible across user + workspace scopes
---------
Co-authored-by: Aleks-0 <aleks-0@users.noreply.github.com>
* feat(core): add Tool(param:value) permission syntax for parameter-level access control
Introduces key:value parameter matching in permission rules, allowing
users to grant or deny tool access based on specific input parameters.
- Parse key:value pairs from specifiers for literal-kind rules
- Support wildcard patterns (*), multiple params, and mixed syntax
- Thread toolParams through PermissionCheckContext and matchesRule
- Add 11 unit tests covering parsing, matching, wildcards, and edge cases
- Maintain backward compatibility with existing specifier kinds
Example rules:
Agent(model:opus) # deny agents using Opus model
Agent(coder,model:*) # deny coder-type agents with any model
Bash(git:*) # still works (legacy :* → git *)
Closes#6100
* fix(permissions): resolve PR #6106 review comments for tool param permission syntax
- buildPermissionRules now propagates toolParamMatchers for 'Always Allow' flow
- MCP tool rules now check param matchers after name matching
- Added Object.hasOwn check to prevent prototype chain lookup vulnerability
- Replaced matchesCommandPattern with matchesParamValuePattern for param value matching
- Added diagnostic logging for param matching failures
- Added warnings for empty valuePattern, invalid keys, and non-literal key:value syntax
- Added type checking for non-primitive param values
* fix(core): address critical review feedback on Tool(param:value) permission syntax
- Add 's' flag to RegExp in matchesParamValuePattern for multiline support
- Filter buildPermissionRules to stable param keys only (model, subagent_type, skill, server_name) to prevent sensitive data leakage
- Reject MCP rules with unsupported specifiers instead of silently ignoring
- Fix :* wildcard conversion to use global replace for backward compatibility
- Extract shared evaluateParamMatchers helper to deduplicate MCP and standard branches
* fix(permissions): address remaining review feedback for Tool(param:value) syntax
- Remove unused @ts-expect-error in gitWorktreeService.ts (CI blocker)
- Fix ReDoS in matchesParamValuePattern: replace regex with linear-time
glob matcher using indexOf, avoiding catastrophic backtracking on
multi-wildcard patterns like *a*a*a*a*b
- Fix MCP backward compatibility: exclude MCP tools from key:value parsing
in parseRule and buildPermissionRules to preserve existing MCP deny
rule semantics
- Add tests for MCP + param matcher, partial wildcards, ReDoS prevention,
number coercion, and buildPermissionRules with toolParams (stable params,
volatile params, sensitive data, round-trip)
* fix(permissions): address PR #6106 review comments and fix useStatusLine test timeout
- Make matchesParamValuePattern case-insensitive to match matchesDomainPattern convention
- Remove duplicate JSDoc block before matchesParamValuePattern
- Remove dead server_name from stableParamKeys in buildPermissionRules
- Add PermissionManager integration tests with toolParams (evaluate, findMatchingDenyRule, hasRelevantRules, hasMatchingAskRule)
- Add type guard tests for evaluateParamMatchers (null, undefined, boolean, object)
- Fix useStatusLine.test.ts timeout by stubbing cron-task exports in core mock
---------
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(cli): support stacked slash-skill invocations (#6355)
Allow users to chain multiple slash-skill commands in a single prompt
(e.g. `/feat-dev /e2e-testing implement X`). The skill bodies are
concatenated with the remaining user text and submitted as one
`submit_prompt` to the model, so the model receives all loaded skill
contexts at once.
- Add `parseStackedSlashCommands()` with a `MAX_STACKED_SKILLS = 5` cap
- Integrate stacked detection into both interactive (slashCommandProcessor)
and non-interactive dispatch paths
- Record `recordSkillInvocation` telemetry for each stacked skill
- Emit a warning when more than 5 skills are requested
- 29 new test cases covering parsing edge cases and both dispatch paths
Closes#6355
* fix(cli): address review feedback for stacked skill invocations
- Fix whitespace tokenization to match all \s chars, not just spaces
- Align telemetry recording: record success based on actual result type
(both dispatch paths now consistent)
- Surface error messages from non-submit_prompt skill results
- Propagate modelOverride from first submit_prompt skill
- Add 7 new tests: tab whitespace, mixed whitespace, non-submit_prompt
exclusion, telemetry accuracy, modelOverride propagation
* fix(acp-bridge): use static import in logRedaction test to avoid timeout
The dynamic `await import('./spawnChannel.js')` inside the test body
was pulling in a heavy module graph at runtime. Under CI contention
(667 tests running concurrently), this exceeded the 5-second default
timeout. Convert to a static top-level import — the same pattern used
by spawnChannel.test.ts.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): move stacked skill block inside try/finally and collect onComplete callbacks
- Move stacked skill dispatch into try block so finally cleanup runs
(setIsProcessing, chat recording, telemetry) preventing TUI freeze
- Set invocationSentToModel=true for stacked invocations so chat
history correctly classifies the command as sent to model
- Collect and forward onComplete callbacks from all submit_prompt
skill results in both interactive and non-interactive paths
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(scheduled-tasks): run each task in its own dedicated, named session
Scheduled tasks created through the Web Shell management page were never firing
in the daemon-only case: the durable-cron tick runs inside an active agent
session, and the Web Shell creates a session only lazily on the first prompt, so
a task created on the management page (with no chat open) had nothing ticking it.
This binds every management-page task to a dedicated session, minted at create
time and named "⏰ <task>". The task fires ONLY inside that session — its
transcript is the task's run history — instead of via the shared per-project
durable owner. A daemon-side keepalive heartbeats those sessions so the idle
reaper doesn't stop them, and a boot-time rehydration reloads them after a
restart. Archiving, deleting, or unarchiving the session disables, removes, or
re-enables the bound task (covered on both the REST and ACP surfaces).
Also adds task editing, a live next-run countdown, run history, a one-per-row
card layout, and a "run now" that executes in the task's bound session and
updates the last-run time. All resident-session management is opt-in and enabled
only by the real daemon (runQwenServe), so createServeApp embeds/tests are
unaffected.
* fix(scheduled-tasks): address code review on per-session task feature
Review fixes for #6389:
- Distinguish archive-disabled from user-disabled tasks: disableTasksForSessions
now marks disabledByArchive; enableTasksForSessions only re-enables tasks
carrying that flag, so a task the user deliberately disabled stays disabled
across an archive/unarchive cycle. [Critical]
- Rehydrate task sessions concurrently with a per-session 30s timeout so one
hung loadSession can't stall the boot sweep or leave healthy tasks dormant.
[Critical]
- Await runScheduledTask + reload before executing the prompt in handleRunNow,
so a record failure surfaces and the card's "last run" reflects the trigger.
[Critical]
- Log keepalive/rehydrate read + heartbeat failures at debug instead of
swallowing them silently, so a persistently-failing keepalive is diagnosable.
[Critical]
- Add integration tests: deleteDaemonSessions -> removeTasksForSessions and
unarchiveDaemonSessions -> enableTasksForSessions (guard the coupling). [Critical]
- DELETE route: single atomic updateCronTasks that captures the bound session
and removes the task in one cycle, closing the read-then-remove TOCTOU.
- Stop the keepalive timer during shutdown (matters for embedders that don't
process.exit) so it can't fire against a disposed bridge.
- Deduplicate DEFAULT_BUILDER: export it once from scheduledTasksSchedule and
drop the dialog's copy so the create form and cron-reversal can't drift.
- Reject empty-string sessionId in isValidTask: a bound task with "" would
silently run unbound under the scheduler's truthy guard.
* fix(scheduled-tasks): isolate task sessions, fix catch-up/jitter/revive
Second review round (#6389):
- Force `sessionScope: 'thread'` when minting a task's session. The daemon's
default scope is 'single', which attaches to (reuses) the shared workspace
session — so a second task, or a task alongside an open chat, would bind to
the same session, rename it, land runs in the wrong transcript, and close it
on delete. Thread scope guarantees each task an isolated session. [Critical]
- Re-seat a recurring task's schedule anchor to now when a PATCH changes its
cron (or flips one-shot→recurring), not just on re-enable. A bound task's
catch-up runs on every file-watch reload, so a bare cron edit to an
expression with an already-past slot would fire immediately on save. [Critical]
- Revive a non-resident bound session from the keepalive when its heartbeat
fails (reaper let it go while disabled/archived, now re-enabled). Covers the
unarchive and PATCH false→true paths uniformly and retries each interval, so
a re-enabled task actually resumes instead of showing a live countdown that
never fires. Best-effort, timeout-bounded, non-blocking. [Critical]
- Report `nextRunAt` using the scheduler's jittered fire time
(`nextDurableFireMs`) instead of the bare cron boundary, so the UI countdown
lines up with the real fire (the tick offsets each fire by up to the jitter
window) rather than expiring early and advancing prematurely.
All four are mutation-verified. The cross-daemon double-fire on bound tasks
(same session live in two schedulers) is a separate, architecturally-invasive
fix (claim-then-fire on the durable file) tracked as a follow-up.
* fix(scheduled-tasks): sync bound session name on task rename
Create names a task's session after the task (`⏰ <name>`), but a later PATCH
that renamed the task (or edited the prompt of an unnamed task) left the
session's display name stale. The PATCH route now re-applies
`updateSessionMetadata` with the task's effective label whenever that label
actually changes — a bare cron/enabled edit does not touch the session.
Best-effort: a metadata failure doesn't fail the committed schedule change.
Mutation-verified.
* fix(scheduled-tasks): mirror run sessionId on client type; clarify server wiring
Review follow-up (#6389, qqqys):
- [Medium] `DaemonScheduledTaskRun` now mirrors the daemon's `CronTaskRun`
`sessionId?: string`, so run-attribution the wire already sends isn't silently
dropped by the client type (not surfaced in the UI yet; passthrough cast means
no mapping change needed).
- [Nit] Comment the `app.locals.stopScheduledTaskKeepalive` set site, noting it
follows the same convention as `fsFactory`/`boundWorkspace`/`acpHandle` and is
read by the run-qwen-serve shutdown path (kept the convention rather than
diverge to a one-off return value / declaration merge).
- [Nit] Comment the outer `.catch(() => {})` on rehydrate as intentional
defense-in-depth (the function already handles read + per-session failures).
* fix(scheduled-tasks): couple archive/enable + record manual run only on enqueue
Two [Critical] review items (#6389, gpt-5-codex):
- PATCH re-enable coupling: reject `enabled: true` on a task disabled BY
archiving its session (`disabledByArchive`) with 409 `task_session_archived`.
Re-enabling it here would show an enabled task with a countdown while its
bound session stays archived and can never fire — the caller must unarchive
the session (which clears the marker and reloads it). A user-disabled task
(no marker) and non-enable edits are unaffected.
- Manual "run now" ordering: record the run only AFTER the prompt is enqueued,
not before. `runTaskManually` now returns a promise that resolves on enqueue
and rejects if the bound session can't be opened (archived/deleted), is
superseded, or times out; the dialog awaits it before writing
/scheduled-tasks/:id/run, so a failed session switch no longer leaves a
phantom run in history. Runs are serialized (one pending at a time, button
disabled) so two quick clicks can't drop a prompt on the single bound-run
latch. Added coverage for failed session load and double-click; all new
tests mutation-verified.
* fix(scheduled-tasks): close dormancy/orphan/overflow gaps from review
Five items from GPT-5 /review (#6389):
- [Critical] Bind tasks to sessions only when resident management is on:
createServeApp now passes the bridge to the scheduled-task routes only when
`manageScheduledTaskSessions` is set. Embedders that leave it off get UNBOUND
tasks (shared-owner firing) instead of bound tasks nothing keeps resident or
reloads (which would silently go dormant).
- [Critical] Keep the keepalive/revive loop running whenever task sessions are
managed, not only when a reaper is active — archiving closes a task session,
so a re-enabled one still needs reviving with the reaper disabled. Size the
interval under the reaper window (≤ half of it) so a small idle timeout can't
let a session be reaped before its first heartbeat.
- [Critical] Record a manual run only after the prompt is admitted: the bound
run latch now resolves only if `sendPrompt` admitted the prompt and rejects on
cancellation (e.g. onSubmitBefore) / failure, so a cancelled Run now no longer
advances lastFiredAt or appends history.
- [Critical] Clamp the dialog's reload timer to the 32-bit setTimeout ceiling
(~24.8 days) so a months-away schedule can't overflow and spin a reload loop.
- [Suggestion] Pre-check the task cap before spawning a session, so an over-cap
create never mints an orphan task session it must roll back.
New tests (route unbound-when-no-bridge, cap-no-spawn, computeKeepaliveIntervalMs
bounds, far-future timer clamp) mutation-verified; full server suite green.
* fix(scheduled-tasks): guard catch-up double-fire, run-now hang, /run + cron edits
Four items from GPT-5 /review (#6389):
- [Critical] Bound-task catch-up could double-fire: detection ran on every
file-watch reload and read the stale on-disk lastFiredAt, so a reload racing
the async catch-up persist (a foreign write to the tasks file) re-detected and
re-fired the same overdue slot. Track ids whose catch-up was DELIVERED but not
yet persisted (`deliveredCatchUp`) and skip re-detecting them until the write
lands; a merely-buffered-then-dropped catch-up isn't tracked, so it still
re-detects from disk (recovery preserved).
- [Critical] "Run now" hung the full 30s switch timeout when the bound session
was ALREADY the current, loaded one (no dep change → the consuming effect
never re-ran). Fire the enqueue directly after loadSidebarSession resolves as
well as from the effect; whoever runs first nulls the latch, so it runs once.
- [Critical] POST /run recorded a run with no enabled/disabledByArchive guard,
unlike PATCH — a direct API caller could write a phantom "ran" record onto a
paused/archived task. Return 409 task_disabled for a disabled task.
- [Suggestion] Anchor re-seat on cron edit compared the raw string, so a
cosmetic change (`0 9 * * *` → `00 9 * * *`) dropped a pending catch-up.
Compare the canonical (parsed) schedule instead.
(The setTimeout-overflow and keepalive-floor reports were already fixed in
2a12cba.) New tests for the first three + the cosmetic-cron case are
mutation-verified; full core scheduler + route suites green.
* fix(scheduled-tasks): block disabled-task run in UI; record manual run at admission
Two [Critical] review follow-ups (#6389):
- A disabled task could still EXECUTE from the Web Shell: the Run button was
only gated on `runningTaskId`, so clicking it enqueued the prompt and the
server's `/run` `task_disabled` guard merely refused the later history write —
a real, unrecorded run. Gate `handleRunNow` and disable the button on
`!task.enabled` too, so a disabled task's prompt is never enqueued.
- Manual run recorded only after the whole turn: the bound-run latch resolved
via sendPrompt, which completes through waitForAcceptedPromptCompletion, so a
long/permission-blocked run or a closed tab could execute without ever being
recorded. Add an `onAdmitted` callback to sendPrompt (fired when the daemon
accepts the prompt, before the turn) and resolve the manual-run latch at
admission instead — cancellation before admission still rejects.
New dialog test (disabled task → no enqueue) mutation-verified; webui/web-shell
typecheck + existing session-action tests green.
* fix(scheduled-tasks): guard tick double-fire, cap rehydration, harden lifecycle writes
Review follow-ups (#6389):
- Extend the fire-persist re-detection guard to ON-TIME tick fires, not just
catch-ups (renamed deliveredCatchUp → firePersistPending): a bound task fired
by the tick advances lastFiredAt asynchronously, so a reload racing that write
(bound detection runs every reload) could re-detect the slot and double-fire.
The tick persist now adds its ids to the guard and clears them when the write
lands, symmetric to the catch-up persist.
- Bound boot-rehydration concurrency (batches of 4): each loadSession forks a
child, so loading up to 50 at once spiked the host and risked spawn failures
that strand tasks. The keepalive revive path was already sequential.
- Archive disable failure is now logged (was fully swallowed) so a broken
archive→pause coupling — where the keepalive would revive the just-archived
session — is diagnosable.
- Unarchive re-enable failure is surfaced in the result `errors` and logged, and
enableTasksForSessions also runs for already-active sessions — so a task left
stranded ({enabled:false, disabledByArchive:true}) by a prior failed enable is
recoverable by re-unarchiving, instead of being permanently stuck.
- Create rollback now removes the persisted session (close + removeSession), so
the loser of a concurrent create at the cap boundary (passes the pre-check,
loses the authoritative write) doesn't leave an orphan named session.
New tests (tick-fire guard, bounded rehydration, already-active recovery)
mutation-verified; full core scheduler + serve suites green.
* fix(scheduled-tasks): one-shot run/edit correctness; tick persist non-regression
Review follow-ups (#6389, ci-bot):
- [Critical] Manual /run on a ONE-SHOT task now removes it from the store. Its
slot is still in the future, so stamping lastFiredAt=now didn't stop the
scheduler firing it again at its original time — a double run. A one-shot's
manual run IS its single fire, so the task is spent.
- [Critical] PATCH recurring:false now re-seats the one-shot's createdAt anchor.
The old (long-past) anchor made the scheduler read it as a MISSED one-shot and
fire + permanently delete it. Re-seating createdAt points its next fire at the
upcoming occurrence. Also covers a cron edit on an existing one-shot.
- [Suggestion] The tick persist no longer regresses lastFiredAt: it skips the
write when the on-disk stamp is already >= the tick slot (a concurrent manual
/run or catch-up may have stamped newer), mirroring the catch-up persist guard.
- [Suggestion] Added the missing create-rollback test: a post-spawn commit
failure closes AND removes the minted session (no orphan).
New tests (one-shot run removal, recurring→one-shot re-seat, rollback teardown)
mutation-verified; core scheduler + route suites green.
* fix(scheduled-tasks): ref-count fire guard, real rehydration cap, authoritative run check
Four [Critical] review follow-ups (#6389):
- Ref-count firePersistPending (was a boolean Set): the same task can have two
lastFiredAt persists in flight (fired again before the first write landed);
clearing on the first settle dropped the guard while the second was still
pending, re-opening the double-fire window. The count holds it until the last
persist settles.
- Rehydration concurrency is now enforced on the REAL loads: loadSession isn't
abortable, so a timed-out load kept forking in the background while the next
batch started. A bounded worker pool holds each slot until the underlying load
actually settles, so in-flight child spawns never exceed the cap.
- Unarchive recovery reports failures for the full resume set: it enables both
unarchived AND already-active sessions but only logged/returned errors for
unarchived, so a failed already-active recovery surfaced errors:[] and left a
task stranded. Deduped one list used for the call, log, and errors.
- Manual "run now" re-checks server-authoritative state before enqueuing: the
dialog snapshot can be stale (another tab/API disabled/deleted the task), so
it would execute the prompt and only the /run record would 409. It now
refreshes, bails if gone/disabled, and enqueues the FRESH prompt/session.
New tests (ref-count, slot-held-past-timeout, stale-disabled re-check)
mutation-verified; core scheduler + serve + dialog suites green.
* fix(scheduled-tasks): catch-up non-regression, disabled-edit re-seat, run/timer/keepalive hardening
Review follow-ups (#6389):
- [Critical] Catch-up persist no longer regresses lastFiredAt: use `>=` like the
tick persist, so a newer stamp (a cross-process manual /run) landing while the
catch-up write is in flight isn't overwritten back to the older minute.
- [Critical] The PATCH anchor re-seat now runs for schedule edits even while the
task is disabled — editing a disabled one-shot's cron then re-enabling it (two
separate requests) no longer leaves a stale anchor that fires + deletes it.
- [High] Manual "run now" of a bound ONE-SHOT consumes it server-side (/run,
which deletes) BEFORE enqueuing, so a record failure leaves a recoverable
"recorded but never ran" instead of a silent double execution at its slot.
- [Medium] The dialog reload timer backs off a stuck past-due nextRunAt (fast
reloads to catch a just-fired advance, then a slow lane) instead of spinning a
1 Hz GET loop.
- [Medium] The manual-run latch bounds the admission phase with a timeout, so a
send that wedges before admission degrades to a visible "run failed" instead
of freezing the run controls.
- [Suggestion] Keepalive: an in-flight guard skips a tick while the previous
pass runs (no duplicate concurrent loadSession spawns), and per-session
exponential backoff stops retrying a permanently-gone session every interval.
New tests mutation-verified. Two deeper items (a task session winning the
durable lock and firing unbound tasks; tearing down a consumed one-shot's
session) are left open as tracked follow-ups — both need new daemon↔child
infrastructure.
* fix(scheduled-tasks): one-shot anchor on unarchive, memoize next-fire, sanitize + log
Review follow-ups (#6389):
- [Critical] enableTasksForSessions now re-seats a ONE-SHOT's createdAt anchor
(not just recurring's lastFiredAt) on unarchive — otherwise unarchiving a task
that was converted to recurring:false while disabled fires it as a missed
one-shot and permanently deletes it.
- [Critical] Log the DELETE-path removeTasksForSessions failure (was fully
swallowed) like the archive/unarchive paths — the session is already gone, so
a silent write failure leaves the still-enabled bound task a permanent ghost.
- [Medium] Memoize nextDurableFireMs (deterministic per id/cron/recurring/anchor)
— a sparse cron costs hundreds of ms per scan and the route recomputed it per
task on every request, stalling the event loop for 50 yearly tasks.
- [Nit] The consumed one-shot /run response now nulls nextRunAt (it was
advertising a future fire on an entity the next GET omits).
- [Suggestion] scheduledTaskSessionName strips terminal control sequences (the
bridge title guard rejects them → silently drops the rename) and truncates on
a code-point boundary (no lone surrogate broadcast as U+FFFD).
- [Critical/doc] Document that firePersistPending is instance-scoped — the
narrow cross-instance restart window is an accepted edge.
- Added the missing test for editing an enabled one-shot's cron.
New tests mutation-adjacent; suites green. Two deeper items (session deleted
outside the daemon orphaning a bound task; surfacing bound tasks in cron_list)
are left open as tracked follow-ups.
* fix(scheduled-tasks): re-seat one-shot anchor on re-enable; guard duplicate revive
Two [Critical] review follow-ups (#6389):
- Re-enabling a one-shot now re-seats its createdAt anchor (added justReEnabled
to the one-shot branch). A one-shot disabled past its slot then re-enabled was
otherwise read as a missed one-shot on the next reload — fired immediately and
permanently deleted. Updated the prior "leaves anchor untouched" test to the
safe behavior (fires at next occurrence).
- Keepalive revive no longer spawns a duplicate child: loadSession isn't
abortable, so a timed-out revive keeps running; a later tick (past its backoff)
would start a SECOND load for the same session. An in-flight `reviving` set
(cleared on the load's TRUE settlement, not the timeout) blocks that — without
holding the sequential tick, so other sessions' heartbeats aren't delayed.
Added a configurable reviveTimeoutMs for the test.
Both mutation-verified. (The one-shot /run session teardown raised again is the
same item as the open deferral — a synchronous close there would break the run,
which executes after /run; it's tracked for the keepalive orphan-sweep.)
* fix(scheduled-tasks): strip bidi override/isolate chars from session name
The bridge's title guard (hasControlCharacter) only rejects C0/DEL, so
Unicode bidi override/embedding/isolate controls (U+202A–202E, U+2066–2069)
slip past it and can visually reorder a scheduled-task session name in the
session list — a Trojan-Source-style attack (CVE-2021-42574). Strip them
alongside the existing terminal-control-sequence pass, matching core's
stripDisplayControlChars canonical set.
Adds a test built from code points so the test file itself carries no
reordering controls.
* fix(scheduled-tasks): close review findings — rehydrate deadlock, manual-run recording, shared helpers, tests
Addresses the review findings on the per-task-session work:
- keepalive rehydrate no longer awaits a non-abortable loadSession after its
timeout. A genuinely hung load would pin its worker and, with enough hangs,
wedge the whole boot sweep (Promise.all never settles) so later task sessions
never rehydrated. The worker now records the timeout as failed and pulls the
next queued session; the background load is left to settle. Rewrote the test
that pinned the old "hold the slot" behavior into a no-wedge regression guard.
- web-shell manual run drops its pre-admission timeout. sendPrompt isn't
abortable, so rejecting on the timer while the send was still in flight let a
LATE admission execute an UNRECORDED run the user could retry into a
duplicate. The run is now tied to admission (accepted prompts are always
recorded); the "session never becomes active" phase stays bounded by the
switch timeout in runTaskManually.
- extract collectBoundSessionIds() shared by the heartbeat + rehydrate passes
(was duplicated) and isBoundTask() in the lifecycle module (was the lone
`sessionId !== undefined` check vs. the strict one used everywhere else).
- spell the nextDurableFireMs cache-key separator as `\x00` rather than a
literal NUL byte, so cronScheduler.ts no longer reads as binary to ripgrep.
- add App.test coverage for the manual-run orchestration (admission-resolve,
cancel/error reject, immediate fire, supersede, switch timeout) and a
keepalive test that a disabled task gets no heartbeat and no revive.
* fix(web-shell): "create via chat" opens a fresh session in scheduled tasks
The scheduled-tasks "Create via chat" button switched to the chat view but
stayed on the CURRENT session, piling the task-creation conversation onto
whatever the user was already doing. It now starts a new session first
(createNewSession) and jumps to it before priming the composer, so task
creation gets its own chat. Covered by a new App.test case asserting
clearSession() is called.
* fix(scheduled-tasks): address follow-up review findings
- keepalive rehydrate: guard the onError callback with try/catch. If it threw
(e.g. stderr EPIPE during log rotation) the rejection escaped loadOne, failed
its worker, and short-circuited Promise.all — stranding every other queued
session.
- cronScheduler catch-up: use the strict `typeof sessionId === 'string' &&
length > 0` bound-check instead of `!== undefined`, matching every other
"is bound?" site.
- server rehydration: log the outer defense-in-depth catch instead of swallowing
it, so an unexpected throw isn't a silent "tasks never fire".
- session-name sanitizer: also strip the standalone Bidi_Control marks U+061C /
U+200E / U+200F, not just the override/isolate ranges.
- scheduled-tasks dialog: when a consumed one-shot then fails to deliver, show a
specific "deleted but never ran — recreate it" error instead of the generic
"run failed" that hid the deletion. Kept the deliberate consume-first ordering.
* fix(web-shell): don't prime the composer when "create via chat" can't start a new session
onCreateViaChat's deferred composer-priming ran unconditionally: if
createNewSession() failed, the task-starter text was dropped into the CURRENT
session (only onSessionIdChange was gated on success). Gate all post-create
side effects on `created`, matching handleMissingSessionNewSession. Adds an
App.test failure-path case (new session fails → composer not primed).
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Design docs and implementation plans were scattered across .qwen/design,
.qwen/plans, and docs/superpowers. The .qwen/ locations are git-ignored, so
docs written there never got tracked, while docs/design already held the
richer, version-controlled set. Consolidate everything under docs/design and
docs/plans, relocate two stray root docs into docs/design, and repoint the
references left dangling by the move (moved-doc cross-links and a few source
comments).
Also update AGENTS.md and the feat-dev skill so the documented workflow writes
new design docs and plans to the tracked docs/ locations.
Co-authored-by: DragonnZhang <dragonzhang1024@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Reorder reminderParts in getInitialChatHistory() so stable parts (MCP instructions, skills snapshot, startup context) come first and volatile deferred-tools reminder is last — prefix-caching servers retain the KV-cache for the shared prefix, only the tail recomputes.
Co-authored-by: Aleks-0 <aleks-0@users.noreply.github.com>
* feat(cli): add --project and --global flags to /model for per-project model persistence
Add scope control to the /model command so users can persist model
selections to either project-level or user-level settings independently.
- /model --project: persist to workspace .qwen/settings.json
- /model --global: persist to user ~/.qwen/settings.json
- /model (no flag): unchanged behavior (backward compatible)
- Model dialog title shows scope: 'Select Model (this project)' / 'Select Model (global)'
- Completion and argumentHint updated with new flags
- Full i18n support for zh/en
Closes#6052
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): add missing zh-TW translations for /model scope flags
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): address PR review — scope flags, subcommand persistScope, titles, tests
- parseScopeFlags: use (?:^|\s) instead of \b for --flag matching
(\b fails because - is not a word character)
- Completion: strip all flags to isolate model prefix, supports any order
- Subcommand dialogs (fast/voice/vision) now propagate persistScope
- slashCommandProcessor forwards persistScope for all subcommand cases
- ModelDialog title combines subcommand mode + scope label
e.g. 'Select Fast Model (this project)'
- Subcommand confirmations show scope suffix (project/global)
- Extract persistScopeSpread() helper to reduce duplication
- Add 9 tests covering scope flags, dialog returns, confirmations
- Add i18n keys for scope suffix labels in zh/en/zh-TW
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): use Partial<Config> & {[key:string]:unknown} to fix index signature TS error
Replace Record<string,unknown> with Partial<Config> & {[key:string]:unknown}
to satisfy TS4111 index signature access rule in the CI build.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): add scope suffix to ModelDialog history items
Address review comment: historyManager.addItem for voice/fast/vision/main
model selections now shows scope indicator like ' (this project)' or
' (global)', consistent with CLI direct-set confirmations.
Affected: handleModelSwitchSuccess (main), handleSelect (voice/fast/vision)
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): wrap scopeSuffix in t() and unify wording with ModelDialog
- scopeSuffix in modelCommand.ts now uses t(' (this project)') / t(' (global)')
instead of hardcoded English strings, matching ModelDialog.tsx wording
- Main model confirmation uses shared scopeSuffix instead of separate
i18n keys, eliminating 'Model: {{model}} (project)' duplication
- Remove unused i18n keys from en/zh/zh-TW locales
- Update tests to expect '(this project)' wording
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): address code review feedback — scope validation, i18n, tests
- Reject inline prompt + scope flag combination with clear error (#1)
- Add mutual exclusivity check for --project and --global (#5)
- Verify setValue scope parameter in tests + add --global test (#2)
- Extract scopeSuffix to shared variable, remove duplication (#3)
- Remove dead i18n keys 'Select Model (this project)' / '(global)' (#4)
- Fix scopeSuffix placement on model line not API key line (#8)
- Add fr.js / ja.js translations for scope keys (#10)
- Remove unused export ModelDialogPersistScope (#6)
- Wrap non-interactive help text in t() with new flags (#7)
- Fix argumentHint grouping to show mode vs scope flags (#11)
Signed-off-by: Alex <alex.tech.lab@outlook.com>
* fix(cli): reject --project when workspace is untrusted
Reject --project scope flag before direct persistence or opening ModelDialog
when settings.isTrusted is false. Workspace settings are ignored on merge in
that state, so the save would silently not take effect.
Also mirrors the guard in ModelDialog.tsx resolvePersistScope() to fall back
to user scope when the dialog is opened with --project on an untrusted folder.
Default mock settings now includes isTrusted: true.
Signed-off-by: Alex <alex.tech.lab@outlook.com>
---------
Signed-off-by: Alex <alex.tech.lab@outlook.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>