The channel proxy path used ProxyAgent, which unconditionally routes
all requests through the proxy and ignores NO_PROXY. This caused
requests to hosts listed in NO_PROXY (e.g. localhost, internal IPs)
to fail when a corporate proxy was configured.
Switch to EnvHttpProxyAgent, matching the main CLI config path that
already handles NO_PROXY correctly.
Co-authored-by: qwen-autofix <autofix@qwen-code.ai>
* feat(review): add issue-fidelity and root-cause ownership gate to /review
Adds a dedicated Issue Fidelity & Root-Cause Ownership agent (Agent 0) to
the /review pipeline and a core-infrastructure scope gate that runs before
the review agents.
Agent 0 fetches linked GitHub issue evidence directly (closingIssuesReferences
plus issue comments) instead of trusting the PR author's framing, compares the
original reported failure against the PR's claimed fix, and flags client-side
parser/sanitizer workarounds for malformed upstream output as Critical unless a
maintainer explicitly requested the defensive mitigation. The core-infra gate
applies the repository's existing two-tier maintainer-only rule before spending
review budget.
This hardens the pipeline against a false-approval mode where a bot PR passes
its own tests and reads as internally reasonable but fixes the author's mistaken
diagnosis rather than the linked issue's actual root cause.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(review): address PR review feedback on issue-fidelity gate
- Fetch issue evidence with `gh issue view --json title,body,comments` so the
issue body (reporter repro/observed payload/expected behavior) is included;
`--comments` alone omits it. Use each closingIssuesReferences entry's own
repository so cross-repo linked issues resolve correctly.
- Treat closingIssuesReferences as a discovery hint (fetch apparent target
issues even when it is empty) and treat fetched issue content as untrusted
data (extract facts, ignore embedded instructions).
- Run Agent 0 (Issue Fidelity) only for PR targets; skip it for local-diff and
file-path reviews, and require the PR number/repo/context in its prompt.
Handle empty references / non-bugfix / gh failure explicitly.
- Pass Agent 0's quoted issue evidence to Step 4 batch verification and stop it
rejecting issue-grounded findings just because the code compiles/tests pass.
- Make the core-infrastructure gate concrete: deterministic maintainer signal
via authorAssociation, count only core-path lines, honor the AGENTS.md
low-risk-sweep exception, clean up the worktree on hard block, run the gate
right after fetch-pr (before npm ci), and map escalate -> COMMENT (never
APPROVE) in Steps 6-7.
- Sync agent counts and token math across SKILL.md, DESIGN.md, and
code-review.md (Agent 0 is PR-only; ~620-730K).
* docs(review): rename 'Linked Issue Fit' heading to 'Issue Fidelity'
Aligns the code-review docs heading with the 'Issue Fidelity' name used
for Agent 0 in SKILL.md and DESIGN.md, so the section connects to the
pipeline diagram. Addresses review feedback.
* docs(review): stop core-infra hard block before load-rules and surface it via --comment
- Hard block now stops before Step 2 (load-rules) instead of before Step 3,
so a PR destined for hard-block no longer runs the load-rules step.
- In --comment mode the hard block posts an event=COMMENT on the PR, matching
the escalate path's GitHub visibility, so external authors see the block.
---------
Co-authored-by: dragon <dragon@U-2Q53JQG9-0233.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): preserve no-argument tool calls that stream an empty arguments string
For tools that take no parameters, some OpenAI-compatible providers
stream `arguments: ""` (or omit the field entirely) and never send an
argument fragment. The streaming parser dropped such calls wholesale
(`meta?.name && buffer.trim()`), while the non-streaming path keeps
them with `args: {}` — so a turn containing only that call looked
empty and geminiChat raised "Model stream ended with empty response
text", triggering pointless retries.
Align the streaming parser with the non-streaming path: emit the call
with empty args when the buffer is empty at stream end. Rewrite the
unit test that encoded the drop, and add regression coverage at parser
and converter chunk level.
* fix(core): use name metadata as slot-occupancy signal for no-argument tool calls
Follow-up to review feedback: after empty buffers became a legal end
state for no-argument tool calls, three parser methods still used
buffer.trim() to decide whether an index slot was occupied. A provider
reusing indices could then silently overwrite a completed no-argument
call (addChunk collision guard, findNextAvailableIndex) or append stray
continuation chunks to it (findMostRecentIncompleteIndex).
Switch the occupancy signal in all three places to the name metadata,
keeping the JSON-completeness check for non-empty buffers. Add
regression tests for both corruption paths and update the stale
getCompletedToolCalls JSDoc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): collapse non-object argument parses at emit and lock canonical empty-opener shape
Review follow-up on the no-ID continuation routing at addChunk. Mid-stream,
an empty buffer with name metadata is formally undecidable between "completed
no-argument call" and "canonical opener awaiting its first argument fragment"
(every OpenAI-compatible provider opens with arguments:"" and streams
fragments ID-less at the same index). Routing must favor the canonical shape,
so the guard stays; a new test pins that shape, which the suite previously
did not cover.
The corruption concern from review is instead bounded at emit time: a buffer
polluted by a stray fragment can parse or repair to a non-object value, which
now collapses to {} so a polluted no-argument call still emits empty args.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): add debug logging for empty-buffer emission and non-object argument collapse
Review follow-up: a stray fragment that happens to parse as a valid JSON
object is indistinguishable from real arguments at emit time, so log both
the non-object collapse and empty-buffer emissions to aid diagnosis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(core): extend replay guard to opener-shaped replays of no-argument tool calls
Review follow-up: after empty buffers became a legal completed state, a
replayed opener (duplicate ID, #5107 lineage) could overwrite a completed
no-argument call's name metadata, since the replay guard only engaged on
non-empty buffers.
Swallowing every known-ID chunk at that state would drop ID-bearing
argument fragments for providers whose opener streams empty arguments, so
the guard uses the protocol shape as discriminator: a chunk carrying a
name but no argument content is an opener replay and is ignored; a chunk
with argument content is a continuation and appends. Regression tests
cover both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(core): cover null/array argument collapse and multi-slot relocation scan
Review follow-up: pin the null and array branches of the emit-time
non-object collapse, and exercise findNextAvailableIndex scanning past
multiple occupied no-argument slots during collision relocation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: FiaShi <FiaShi@fiashideMacBook-Air.local>
Co-authored-by: tomsen-ai <230283659+tomsen-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
When a file is accessed via a symlinked path (e.g., in git worktrees or
monorepos with symlinked directories), conditional rules and skills
keyed on the real path would fail to activate.
Add resolveSymlinkAwareRelativePaths() that returns both the original
and realpath-resolved relative paths, so glob patterns match either form.
Resolve both the file path and projectRoot via realpath to handle macOS
/private/tmp prefix normalization correctly.
Make matchAndConsume() async in both ConditionalRulesRegistry and
SkillActivationRegistry to support the realpath I/O.
Fixes#6356
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(core): surface PreToolUse hook 'ask' as a TUI confirmation
A PreToolUse hook returning permissionDecision:'ask' was treated the same as 'deny'. The hook fires in the execution phase (_executeToolCallBody), after the confirmation flow in _schedule has finished, so an 'ask' could only block the tool as EXECUTION_DENIED instead of prompting the user.
Bounce the tool from the execution phase back to awaiting_approval when a hook asks: build a synthetic 'info' confirmation whose onConfirm routes through handleConfirmationResponse (ProceedOnce re-executes, Cancel cancels). PreToolUse keeps its "before execution" timing — only the 'ask' branch is new; 'denied'/'stop' keep deny-as-error. A non-interactive CLI or background agent cannot prompt, so 'ask' falls back to deny there.
The re-execution after approval skips both the hook re-fire (no infinite re-ask loop) and the non-idempotent path-unescape prelude. A walk-away abort sets a terminal status so the turn cannot hang, and the tool span survives the bounce so it is finalized exactly once.
Tests: add coverage for ask->awaiting_approval, approve->execute-once (no re-ask loop), decline->cancelled, non-interactive/background deny, walk-away abort, single span finalize, and no double path-unescape.
* fix(core): handle PreToolUse 'ask' bounce edge cases from review
Round 1 review of #5629 surfaced edge cases in the bounce mechanism:
- Multi-tool batch hang: a bounced tool approved while a sibling was still executing stayed stuck in 'scheduled'. attemptExecutionOfScheduledCalls now loops, re-checking for newly-scheduled bounce-approved calls after each batch drains.
- Orphaned hook events: the post-approval re-execution generated a fresh tool_use_id, leaving PreToolUse(old)/PostToolUse(new) unpaired. Preserve and reuse the original id across the bounce.
- ModifyWithEditor double-unescape: request.args is unescaped in place before the hook fires, so the ModifyWithEditor branch must skip its own unescape for a bounced tool (it would double-strip escaped metacharacters).
- Missing signal.aborted re-check before bouncing: mirror the confirmation-phase guard so an aborted signal falls through to deny instead of flashing a confirmation nobody can answer.
Tests: multi-tool-hang regression (RED before the loop fix), non-interactive STREAM_JSON and Zed bounce paths, and span-finalize assertions on the walk-away abort test.
* fix(core): keep PreToolUse 'ask' gate when a sibling is auto-approved
Round 2 review: autoApproveCompatiblePendingTools auto-approved every awaiting_approval tool when a sibling was approved with ProceedAlways — including tools bounced by a PreToolUse 'ask'. The bounced tool would be auto-approved and re-executed with the hook skipped (isPostAskReexecution), silently defeating the hook's confirmation gate. Exclude bounced callIds from the auto-approve filter so a hook 'ask' always requires explicit confirmation.
Test: a sibling's ProceedAlways no longer auto-approves a bounced ask (RED before the filter guard).
* fix(cli): preserve hook ask prompts on approval mode change
* fix(core): handle PreToolUse ask edge cases
* fix(core): cancel scheduled calls during ask abort drain
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(cli): keep model picker entries contiguous
* fix(cli): account for the error box when capping model picker rows
The model list's row budget didn't reserve space for the inline error
message shown after a failed switch, so it could still overflow a
short terminal in that state. Also cover the capping formula's
untested branches (floor at very small heights, the two-row
description path, the undefined-height fallback) and the
DescriptiveRadioButtonSelect ReactNode description path introduced by
the same change.
* fix(cli): pad error-row estimate for wrapped error text
errorMessageRows only counted explicit newlines, undercounting rows
when the error Text wraps on narrow terminals. Add a small buffer and
tighten the regression test's assertion to the exact expected value.
* fix(cli): show scroll arrows and document the model dialog row budget
Short terminals can now cap the model list well below its old worst
case of 10, hiding most entries with no indicator that the list
scrolls (unlike ThemeDialog, ApprovalModeDialog, and ArenaStartDialog,
which already show scroll arrows). Enable them here too, and reserve
the 2 extra chrome rows they add. Also document the fixed-rows budget
so future layout changes know to keep it in sync.
* fix(cli): drop model picker scroll arrows when they would crowd out entries
The scroll arrows are two always-rendered chrome rows, so on dialogs too
short to fit them plus a single option row they pushed the option rows
past the dialog's clipped height — the picker showed arrows, title, and
footer but no entries. Hide the arrows in that case and spend their rows
on the list instead. Verified with an E2E height sweep (rows 14-34): at
least one entry is now visible at every height and windows stay
contiguous, with arrows still shown wherever they fit.
* fix(cli): remove model picker scroll arrows to reclaim rows for entries
The ▲/▼ indicators are two always-rendered chrome rows, and in a
height-capped dialog those rows are the scarcest resource — enabling
them cost two visible entries at every constrained height and required
extra logic to avoid crowding out the list entirely on very short
dialogs. Remove them and restore the 14-row chrome budget: the entry
numbering already shows where the visible window sits in the list, and
the footer hint covers navigation. Supersedes the earlier change that
enabled the arrows.
* test(cli): cover the max-item clamp for tall terminals
* feat(web-shell): add token-usage analytics dashboard to Daemon Status
Add a "统计 / Usage" tab to the Daemon Status page: a Today/7D/30D period toggle over the selected range's token totals and input/output/cache-read breakdown, a 12-month token heatmap (per-day tokens + cache-read tooltip, localized month labels), per-model token share, skill-call counts, and daily token/session charts.
Backend: a new read-only GET /usage/dashboard daemon API backed by a core usage-dashboard service that aggregates the durable local usage history (cross-project ~/.qwen), reusing loadUsageHistory + aggregateUsage. Skill counts are threaded through the shared usage pipeline. No new instrumentation — every metric is read from data qwen-code already persists.
* fix(web-shell): address usage-dashboard review feedback
- cap `aggregateUsage` topSkills at 25 like topTools, so the aggregate and dashboard payload stay bounded
- fix a DST drift in the heatmap grid: advance the day/month cursor by calendar day (setDate) instead of a fixed `i * MS_PER_DAY` offset
- cache the loaded history once (range-independent) so toggling Today/7D/30D re-aggregates from a single disk read; split a pure `buildUsageDashboard(records, opts)` out of `loadUsageDashboard`
- drop the unused per-day streak computation and the dead `daemon.usage.streak` i18n key
- add debug logging to the dashboard builder and a direct `aggregateUsage`-skills unit test
* fix(usage-dashboard): make the dashboard load read-only + fix cache coalescing
- Make the daemon dashboard side-effect free: `loadUsageHistory` gains a `persistRebuild` option, and the route passes `persistRebuild: false`, so serving a GET never writes to `~/.qwen`. The transcript-rebuild fallback previously persisted rebuilt records (including an in-progress session), violating the read-only contract.
- Fix cache coalescing on the slow path: a pending history load is now reused regardless of age (the TTL starts at settlement), so a request arriving after the TTL while the load is still pending no longer kicks off a second full load.
- Tests: read-only rebuild writes nothing, `metricsToUsageRecord` copies `SessionMetrics.skills`, and a pending load is shared past the TTL.
* fix(core): default unknown context windows to 200k
* fix(core): only stamp known model context windows in resolver
- Use knownTokenLimit() in the env-var resolver fallback so unknown
models keep contextWindowSize undefined instead of being labeled
'auto-detected from model' with the generic default
- Add resolver tests: known limit differing from the default (gpt-4o),
unknown model stays undefined, settings value not overridden
- Recalibrate the config.getWarnings default-window test for the 200K
fallback
- Sync the vscode-ide-companion copy of DEFAULT_TOKEN_LIMIT to 200K
Add POSIX /tmp to ACP local read fallback roots without changing read_file's default permission behavior. Also add QWEN_ACP_LOCAL_READ_ROOTS as an append-only absolute-path override for ACP fallback reads.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(web-shell): show Settings and Daemon Status as an in-place panel
The Settings and Daemon Status buttons opened centered modal overlays that
dimmed the whole app. Render them as a full-height panel that replaces the
chat surface instead — a Back button or Escape returns to the chat — with the
content left-aligned and filling the chat pane width rather than centered in a
narrow column. On the Daemon Status page, give the time-series charts a taller
plot and the overview cards a wider track so a wide window is actually used;
the tab grouping (overview / metrics / diagnostics) is kept.
* fix(web-shell): preserve composer draft and refine panel focus/escape
Address review feedback on the in-place Settings / Daemon Status panel:
- Keep the chat view (message list + composer) mounted and just hidden while a
panel is shown, so typing a prompt, opening Settings/Status, then going Back
no longer discards the unsent draft and attachments (the composer subtree was
being unmounted and remounted empty).
- Focus the Back button when a panel opens and restore focus to the composer
when it closes, replacing the focus management DialogShell used to provide.
- Reload workspace settings after the fast-model command resolves so the still-
mounted Settings panel doesn't keep showing the previous value.
- Don't close the panel when Escape is handled inside the sidebar (its search
input clears on Escape without stopping the event).
- Guard the overview card grid with min(100%, 340px) so a 340px track can't
overflow a narrow panel.
* fix(web-shell): reset panel scroll when switching Settings/Status
Add key={activePanel} on the panel body so switching directly between Settings
and Daemon Status (activePanel goes 'settings' -> 'status' without passing
through null) remounts the scroll container and opens at the top instead of
inheriting the previous panel's scrollTop.
* fix(web-shell): restore new-chat vertical centering
The chatViewWrap added to keep the composer mounted while a panel is shown was
filling the pane, which cancelled the empty (new-chat) state's vertical
centering (welcome header + composer stuck to the top instead of centered).
Shrink the wrapper to its content in the empty state so
`.appChatEmpty .chatPane { justify-content: center }` centers it again, matching
how `.content` behaves there.
* fix(web-shell): restore session-org test destructuring dropped in merge
My earlier merge of origin/main 3-way-combined WebShellSidebar.test.tsx
incorrectly, dropping the `{ }` destructuring from the 7 session-organization
tests. renderSidebar returns `{ container, rerender }` (not the element), so
`const container = renderSidebar(...)` made `container.querySelector` throw.
Restore the file to origin/main so the tests pass again.
* fix(web-shell): surface pending approvals over Settings/Status panel
The in-place Settings/Daemon Status panel hides the chat footer with
display:none, and the ToolApproval / AskUserQuestion overlays live in
that footer. A gated tool call that arrived while a panel was open
rendered the approval into a hidden container, so the turn hung with no
visible prompt (reported [Critical]).
Close the panel when an actionable approval is pending so it surfaces.
Only actionable approvals count (pendingToolApproval / pendingAskUserApproval
already gate on canActOnPendingApproval), so a non-owner in a shared
session isn't yanked out of Settings by someone else's prompt. Leave
focus for the overlay rather than the composer on this path: ToolApproval
uses a window-level key handler that ignores editable targets, so
focusing the composer would swallow its shortcuts.
Also refocus the Back button on panel->panel switches (not just on open),
so focus no longer relies on the Back button being the same DOM node
across the keyed panel body, and expose SvgLineChart's plot height as
--chart-height (fallback 140px) so a constrained caller can shrink it.
Adds App-level tests: the panel auto-closes on a pending tool approval,
and stays open when the only block is a resolved (non-actionable) one.
* test(web-shell): cover AskUserQuestion auto-close; robust panel selector
Follow-up to the approval auto-close fix, addressing review suggestions:
- Add data-testid="inline-panel" to the panel <section> and query by it in
App.test instead of querySelector('section'), which would false-positive if
any future component renders a <section>.
- Add a companion test for the pendingAskUserApproval branch of the auto-close
effect. The ask-user block carries toolCall.input.questions so
isAskUserPermission() classifies it as an AskUserQuestion; mutation-checked
(restricting the effect to pendingToolApproval fails only this test).
- Document why handleFastModelSelect's post-sendPrompt reload is not racy:
sendPrompt awaits waitForAcceptedPromptCompletion, so the /model --fast turn
has already applied when reloadWorkspaceSettings() runs. Also note why the
command path needs the explicit reload that the setWorkspaceSetting pickers
(vision/voice) get for free via the settingsVersion signal.
* fix(web-shell): close inline panel when resuming a session
The /resume <id> command and the ResumeDialog onSelect both call
loadSession() without closePanel(), unlike createNewSession and
loadSidebarSession. Loading a session means the user wants that chat, so
leaving a Settings/Daemon Status panel open would hide it. Add closePanel()
to both paths for consistency (a no-op when no panel is open).
Currently reachable only in theory — the composer that submits /resume is
display:none while a panel is shown — but the guard keeps the invariant if
a non-composer entry point (sidebar/shortcut) is ever added.
Adds a mutation-checked test (removing closePanel from the /resume handler
fails it) and gives the ChatEditor test mock a focus() method, since the
panel-close focus effect now runs editorRef.focus() on this no-approval path.
* fix(web-shell): keep composer dormant while an approval overlay is up
Follow-up to the approval auto-close fix. When an approval arrives while a
Settings/Status panel is open, the auto-close clears activePanel, so
interactionBlocked flips false and useComposerCore's dialogOpen effect
refocuses the still-mounted composer. ToolApproval ignores approval
shortcuts from editable targets, so the now-visible approval stops
responding to Enter/Escape/number keys until focus moves away.
Key the ChatEditor dialogOpen prop off the pending approval too, so the
composer stays blurred while an approval owns the keyboard. Consolidate the
"an approval overlay is active" condition — previously duplicated in the
auto-close effect and the panel focus guard — into a single
approvalOverlayActive value so the three consumers can't drift.
Uses the actionable-approval condition (pendingToolApproval /
pendingAskUserApproval) rather than raw pendingApproval, matching the
auto-close gating and avoiding a blurred composer when a non-actionable
approval renders no overlay.
Adds a mutation-checked test asserting dialogOpen is true while an approval
is pending with no panel open.
* test(web-shell): cover the Daemon Status panel branch
A review note observed that all five panel tests open via /settings, so the
activePanel === 'status' branch (DaemonStatusDialog) had no coverage — a
regression in the 'status' literal would go undetected. Add a test that
opens Daemon Status through the sidebar and asserts the panel opens and
auto-closes on a pending approval, exercising that branch and confirming the
auto-close is panel-type-agnostic.
Gives the WebShellSidebar test mock an onOpenDaemonStatus button, since
there is no slash command to open the status panel. Mutation-checked:
neutering setActivePanel('status') fails only this test.
* fix(web-shell): dismiss stacked dialogs on approval, focus overlay, a11y
Addresses a review round on the inline Settings/Status panel.
[Critical] When an approval arrives while a DialogShell sub-dialog (model
picker / approval-mode picker) is open over the panel, the auto-close
removed the panel but left the sub-dialog backdrop covering the footer
approval overlay, so the turn hung. Worse, the approval-mode picker stayed
usable — selecting "yolo" auto-approved (handleSetMode) a tool call the
user never saw. Dismiss the panel AND both sub-dialogs when an actionable
approval is pending.
[Critical] After the panel closes for an approval, focus fell to <body>
(the Back button unmounted; ToolApproval has no autofocus). Move focus onto
the ToolApproval overlay wrapper (tabindex=-1, so its window-listener
shortcuts keep working and Enter doesn't confirm early) once it is visible.
AskUserQuestion keeps managing its own focus.
Suggestions:
- Guard reloadWorkspaceSettings() rejection (was unhandled).
- aria-hidden the display:none chat view so AT can't wander into it.
- Close the panel before /model --fast so its response shows in the chat
in context instead of piling up behind the hidden panel.
- Remove the dead .panelBodyInner wrapper (redundant width:100%).
- Rename data-mobile-drawer -> data-sidebar-shell (it wraps the desktop
sidebar for all viewports) across App.tsx + standalone.css.
Tests (+5, mutation-checked): sub-dialog dismissal on approval, overlay
focus, Escape closes panel / sidebar-Escape does not, dialogOpen while a
panel replaces the chat. Adds an observable DialogShell test mock.
* fix(web-shell): restore composer focus after approval resolves; cleanups
[Critical] The panel focus effect consumed prevActivePanelRef to null on the
approval auto-close (correctly skipping editor focus then). When the approval
later resolved with no panel to return to, neither branch fired and focus was
left on <body> — the visible composer took no keyboard input. Track the
approvalOverlayActive transition too and restore composer focus on
approval-resolve-after-panel-close. (useComposerCore's dialogOpen effect also
covers this; the extra branch makes the panel effect self-contained.)
Suggestions:
- Log the reloadWorkspaceSettings() rejection instead of swallowing it.
- Remove the dead :global([data-dialog-fullscreen]) .grid rule (the
allowFullscreen source was removed when Daemon Status became a panel).
Tests (+4, focus-restore mutation-checked): focus restored after an approval
resolves post-auto-close; Back-button closes the panel and restores focus;
fast-model pick closes the panel, sends /model --fast, and reloads settings;
chat view is aria-hidden while a panel is shown. Adds interactive
SettingsMessage / ModelDialog mocks and stable editor-focus / settings-reload
spies.
* fix(web-shell): make Settings/Status panel and Scheduled Tasks mutually exclusive
Opening Scheduled Tasks (mainView — a position:absolute z-index overlay) and
then Daemon Status (activePanel) rendered the Daemon Status panel *behind* the
Scheduled Tasks overlay, so the button looked like it did nothing. These are
mutually-exclusive full-pane views, so opening one must close the other.
Centralize into openPanel() / openScheduledTasks() helpers (last-opened wins)
and route all six open sites (the /settings and /schedule commands, the three
sidebar handlers, and the StatusBar) through them.
Mutation-checked tests: opening Daemon Status closes the Scheduled Tasks page
(the reported repro), and opening Scheduled Tasks closes an open panel.
* feat(web-shell): add scheduled tasks management page
Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace.
- Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules.
- "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview.
- "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool.
- Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler.
- Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false.
- Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon.
* chore(web-shell): address review feedback on scheduled tasks
- cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one).
- core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel).
- CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload.
- Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them.
- Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation.
* chore(web-shell): address second review round on scheduled tasks
- Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes.
- Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it.
- describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday.
- Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute).
- Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7.
* chore(web-shell): address third review round on scheduled tasks
- Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise.
- Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape.
- Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation.
- Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled.
- Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims).
- Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules.
- Return generic 500 client messages (no internal file path); the detail is logged server-side.
- Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways.
* chore(web-shell): address fourth review round (minor suggestions)
- Route error logs interpolate the actual task id instead of the literal ":id".
- cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names.
- Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog.
- Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard.
- Test generateCronTaskId (format + near-uniqueness).
* chore(web-shell): address fifth review round on scheduled tasks
- Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through.
- describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes".
- Strengthen the corrupt-file route test to assert the generic client message and no leaked file path.
- Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback.
* test(cli): cover legacy scheduled-task normalization on GET
Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files.
* fix(core): cap durable cron loads against a durable-only budget
The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire.
Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads.
* feat(web-shell): named session groups and color tags in the sidebar
Extend web-shell session organization with named groups (create / rename /
delete, assign a session to a group) alongside quick color tags, and surface
pin / archive state. The grouping data is plumbed end-to-end through the
daemon.
- core: session-organization-service carries group id / name / color and
pin / archive metadata on organized-list entries
- sdk / acp-bridge: session-list entries gain groupId / groupName /
groupColor / archivedAt; add SessionGroupColor and list-session-groups
result types
- cli/serve: dispatch + session routes expose listing and assigning groups
- web-shell: sidebar group management UI (create / rename / delete groups,
color picker, pin, archive) and reuse the shared "Group" label for the
group action, dropping the redundant "Move to group" string
* fix(cli): exclude color-tagged sessions from the ungrouped filter
Color / named group / recent are mutually exclusive buckets in the web-shell
sidebar — a color-tagged session shows in its color section, not "recent".
But the organized session-list `group=ungrouped` filter only checked
`groupId == null`, so a color-tagged session with no named group leaked into
ungrouped results for REST/ACP consumers, disagreeing with the UI taxonomy.
Align the server filter: ungrouped means no named group and no color tag.
Adds an ACP session/list test asserting a color-tagged session is excluded
from group=ungrouped (fails on the old filter, passes on the new one).
* fix(web-shell): clear color tag when creating a group for a session
saveGroupEditor's create-with-target path assigned the new group but left any
existing color tag in place, unlike the sibling assignSessionGroup /
assignSessionColor paths that keep color and named group mutually exclusive.
Because color takes precedence in the sidebar's section bucketing, the session
stayed in its color section and the group assignment had no visible effect.
Send `color: null` alongside `groupId` on that path, and extend the
create-group dialog test to assert the assignment clears the color.
* fix(cli): exclude color-tagged sessions from the named-group filter
Follow-up to the ungrouped filter fix: the per-group filter (group=<id>) also
ignored color precedence. Core and the REST/ACP update paths can persist both
groupId and color, and the sidebar renders such a session in its color bucket,
so group=<id> API consumers saw a session the web-shell shows elsewhere.
Require `color == null` there too, matching the sidebar taxonomy
(color > group > recent).
Adds an ACP session/list test for a session with both groupId and color set.
Correct and complete the user-facing settings documentation against
packages/cli/src/config/settingsSchema.ts:
- settings.md: fix general.defaultFileEncoding type (enum, not string);
document the general.voice.* dictation settings, top-level
modelFallbacks and modelPricing, tools.computerUse.idleTimeoutMs,
mcp.toolIdleTimeoutMs, and the skills.disabled denylist.
- model-providers.md: correct the resolution-layers table — only
--openai-api-key/--openai-base-url exist; there are no
provider-specific credential CLI flags.
Co-authored-by: Claude <noreply@anthropic.com>
* perf: memoize skill scans, debounce sleep-inhibitor log, guard IDE readdir (#6134)
Three startup / session performance-noise fixes:
1. **Memoize collectAvailableSkillEntries()**: Add a short-lived (2 s) WeakMap
cache keyed by SkillManager instance. Near-simultaneous callers during
startup (SkillTool, drainSkillAndCommandReminders, buildAvailableSkillsReminder,
coreToolScheduler) now share a single skill scan instead of each re-invoking
listSkills(). The cache is explicitly cleared on refreshSkills() so
skill-set mutations are picked up immediately.
2. **SleepInhibitor one-time latch**: Add exitedWhileActiveLogged flag so the
'exited while active' debug message fires at most once per run. On Windows
the PowerShell inhibitor subprocess may be killed between release()/acquire()
cycles, previously logging on every tool call. The flag resets when
activeCount drops to 0 or on dispose().
3. **IDE client ENOENT guard**: In getAllConnectionConfigs(), catch ENOENT from
readdir on ~/.qwen/ide silently (return []) instead of logging a debug
message on every startup in CLI-only setups without an IDE companion.
* test: add coverage for memo cache, ENOENT guard, and dedup latch
* fix: correct Config import path in skill-utils test
---------
Co-authored-by: 易良 <1204183885@qq.com>
* fix(cli): drop redundant "generating more" cue from the live preview
In non-VP mode the live markdown preview is clipped to a rendered-height
budget so the frame never overflows the viewport and triggers ink's
scroll-to-top full redraw. It used to append a "... generating more ..."
cue (and code/math/mermaid blocks appended their own) to signal that the
clipped tail was still coming.
Since #6170 landed the incremental scrollback commit, that tail is
streamed into <Static> in real time — clipped content is "still
streaming" and reappears within a commit cycle, not "delayed output".
The cue is therefore redundant noise that flickers in step with the
commit cycle, so remove all four occurrences (outer preview clip, code
block, mermaid block, math block).
The row each cue used to occupy is reclaimed for content, so the total
rendered height is unchanged: the code/math/mermaid RESERVED_LINES drop
by one and the outer slice trigger switches from the (now-inlined)
`clipped` flag to `keptLines < allLines.length`. The TableRenderer
"… more rows streaming …" clamp is intentionally kept — an in-progress
oversized table is not yet in scrollback, so that cue still carries
information.
Also gitignore the nested `.qwen/computer-use/` marker so the
auto-generated artifact stops showing up as untracked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): hold back the unterminated table row while streaming
While a markdown table streams, the frontier line is often a half-typed
row like `| a | b` with no closing `|` yet. Because TABLE_ROW_RE requires
both a leading and trailing pipe, that partial line does not match, so the
parser closed the table and rendered the partial as a plain text line
below it — then, once the closing `|` arrived, flipped it into the table.
This per-token flip changed the frame height and re-ran column autosizing
on every keystroke, jittering the live table.
Hold the partial row back instead: when pending, if the final line is an
unterminated table row and at least one complete row already exists, skip
it so `inTable` stays set and the end-of-content handler keeps rendering
the accumulated rows as a live table. The row appears the moment it
terminates. The `tableRows.length > 0` guard keeps the header + separator
from blanking out while the very first row is still being typed.
Note: this smooths the table content itself; it does not change the
streaming repaint frequency, so the fixed bottom controls still repaint on
each tick (that is the domain of the flicker-reduction work, e.g. #5396).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): fix two stale "generating more" cue references in comments
Review follow-up: two comments still referenced the removed outer cue.
- TABLE_PENDING_RESERVED_ROWS: reword "marginY 2 + the outer cue" to
"marginY 2 + one row of wrapped-cell safety headroom". The reserve stays
at 3 on purpose — tables under-estimate their rendered height the most
(wrapped cells), so they keep one more backstop row than the other
blocks; lowering it would shrink that safety margin.
- pending-rendered-height PendingSliceResult.keptLines JSDoc: drop the
"plus a 'more' cue" phrasing — the caller now renders nothing rather
than an oversized row.
Comment-only; no behaviour change. 155 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): hold back the partial first table row too; de-dup reserve constant
Review follow-up on the streaming table hold-back.
- The `tableRows.length > 0` guard skipped the hold-back for the FIRST
data row: a partial first row fell through to the table-closing branch,
which also requires a row, so the header + separator were dropped and
the partial rendered as a stray text line — the same per-token flip the
change is meant to remove, just for the first row. Relax the guard to
`tableHeaders.length > 0` so an unterminated first row/separator is held
back too; the table is simply not drawn until its first row terminates,
then pops in complete and grows one row at a time. Comment corrected to
describe the actual behaviour.
- Add a test for that edge case (partial first row held back, table
appears once the row terminates).
- De-duplicate the magic `3`: the slice-side `tableClampRows` estimate now
references `TABLE_PENDING_RESERVED_ROWS` (moved to the top-of-file
constants) instead of a literal, so the estimate and RenderTable's
render-side `maxHeight` cap can never diverge.
157 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(core): Add LSP server config hot-reload support
- Implement reconcileServerConfigs to diff desired vs current LSP configs and apply minimal add/remove/restart operations with a serialized reconcile queue
- Add configHash utility to detect config changes via stable hashing
- Add lspConfigWatcher in CLI to watch .lsp.json and trigger reconciliation on file changes
- Extend LspServerManager with per-server config hash tracking and detailed debug logging
- Add design docs for LSP runtime reinitialization and hot-reload overview
- Include comprehensive unit tests for all new modules
* refactor(cli): Extract registerLspHotReload from main function
Move the LSP config file watcher setup and reconciliation logic into a dedicated module-private function registerLspHotReload, reducing the size and nesting depth of the main startup flow. Added a JSDoc summarizing responsibilities, early-return conditions, and the AppEvent.LspStatusChanged side effect.
* fix(lsp): release server resources during reload
* fix(lsp): address hot reload review feedback
* fix(lsp): harden hot reload reconciliation
* docs(lsp): update hot reload design notes
* fix(lsp): harden hot reload retry semantics
* fix(lsp): harden hot reload lifecycle
* fix(lsp): harden hot reload lifecycle
* fix(lsp): isolate hot reload recovery paths
* fix(lsp): align command probes and replay tracking
* fix(lsp): prevent crash restarts during shutdown
* fix(lsp): preserve reload state across failures
* fix(lsp): cancel reloads during shutdown
* fix(lsp): handle socket startup races
* fix(lsp): harden command probe env and socket startup
* fix(lsp): report skipped reload and restart states
* fix(lsp): harden hot reload lifecycle cleanup
* chore: add one comment for `Object.create(null)`
---------
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* fix(core): preserve OpenAI reasoning as raw thoughts
* chore(core): remove unused thought text helper
* test(core): cover thought summary branching
* test(core): assert flushed reasoning marker
* fix(core): type tagged reasoning stream state
* feat(core): implement model fallback chain for capacity/availability errors
When the primary model hits 429/503/529 and same-model retries are
exhausted, automatically try configured fallback models in sequence
before giving up.
Config layer:
- Add `modelFallbacks` setting (comma-separated, max 3)
- Add `--fallback-model` CLI flag (repeatable or comma-separated)
- Normalize, deduplicate, cap at 3 in core Config
Core layer:
- Add `isFallbackEligible()` helper to retryErrorClassification
- Update HTTP 529 diagnosis to `fallback-eligible` (was `retryable`)
- Add fallback chain logic in geminiChat `sendMessageStream`:
each fallback gets its own retry budget via `makeApiCallWithFallbackGenerator`
- Resolve fallback models cross-provider via `resolveForModel({ failClosed: true })`
- Skip fallback when `QWEN_CODE_UNATTENDED_RETRY` persistent mode is active
- Non-fallback-eligible errors (auth/client) stop the chain immediately
UI layer:
- TUI notification: "Model X unavailable, falling back to Y"
- Daemon SSE adapter: model_fallback event handling
- Non-interactive: system message with model_fallback subtype
Closes#6116
* fix(core): retry fallback stream failures
* refactor(core): address review findings for model fallback chain
- Delete unused makeApiCallWithFallbackGenerator (merged into
makeApiCallAndProcessStream via overrides parameter)
- Import HeartbeatInfo type for persistent-mode heartbeat callback
- Use popPendingPartialAssistantTurn (removes partial turn from
history) instead of clearPendingPartialState before model switch
- Add AbortError early-return in both fallback catch blocks to
propagate user cancellation immediately
- Reorder guard: check fallbackModels.length > 0 before calling
classifyRetryError to avoid unnecessary work
- Rename errorCode → statusCode in ModelFallbackInfo and all
consumers for consistency with RetryErrorClassification
- Add TODO for retry loop duplication in makeFallbackStreamWithRetries
* fix(core): clean fallback partial turns
* fix(core): address model fallback review comments
* fix(core): tighten model fallback handling
* fix(core): align fallback recovery semantics
* fix(core): tighten fallback failure handling
* fix(core): reduce fallback stream scope
* fix(core): resolve fallback review comments
* fix(core): review polish for model fallback chain
- Add .filter(Boolean) to CLI --fallback-model coerce to strip empty
strings from leading/trailing commas
- Clarify isFallbackEligible() JSDoc: reason-based check intentionally
covers 'retryable' diagnosis (429/503) not just literal
'fallback-eligible' diagnosis (529)
- Preserve original capacity error as cause when all fallbacks exhaust
(previously the cause was the last resolution/fallback error)
- Add test: provider-level rate-limit (numeric code 1302, no HTTP
status) is correctly identified as fallback-eligible
* fix(core): resolve model fallback review comments
* fix(core): dedupe unresolved fallback aliases
* fix(core): trim model fallback scope
* fix(core): stop fallback after emitted output
* fix(core): clear fallback tool call state
* fix(core): skip unresolved fallback aliases
* fix(core): address fallback review feedback
Make tool declaration ordering deterministic so prompt-cache prefixes do not depend on asynchronous registration history.
- Sort function declarations by canonical tool name after existing visibility filtering
- Preserve deferred, revealed, and alwaysLoad filtering semantics
- Add tests covering deferred tools, revealed tools, and MCP registration order
- Document the prompt-cache motivation and next-step cache break detection plan
Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
* feat(web-shell): add onSessionChange and onSubmitBefore callbacks
Add session-level event callbacks and a pre-submit interception hook
to WebShellProps, enabling external consumers to observe session
lifecycle events and gate prompt submissions.
New APIs:
- onSessionChange: fires on rename (SSE-driven), submit (direct and
queued), and turn_complete (streamingState transition with error
context including block ID).
- onSubmitBefore: async hook called before prompt submission; reject
cancels the prompt with full retry-state rollback (lastSubmittedPrompt,
lastSubmittedImages, retriedTurnErrorId, showRetryHint).
Sidebar integration:
- sessionListReloadToken triggers sidebar reload on session events
with pollInFlightRef + document.hidden guards.
- Delayed 2s reload after submit to account for daemon registration lag.
Safety:
- isPreparingPrompt loading state during onSubmitBefore prevents
duplicate submissions.
- streamingSessionIdRef prevents spurious turn_complete on session switch.
- All slash commands (including internal /language, /model) go through
onSubmitBefore; queued prompts intentionally bypass it.
* fix(web-shell): add null initial value to delayedReloadTimerRef
React 19's useRef requires an explicit initial value argument.
Match the existing escapeTimerRef pattern: | null + null.
* fix(web-shell): move clearFollowup after onSubmitBefore gate and add tests
- Move clearFollowup() to after onSubmitBefore succeeds so that
followup context is preserved when the before hook rejects
- Add null guard for clearTimeout on delayedReloadTimerRef
- Add 5 unit tests for sidebar sessionListReloadToken effect
covering: token change, undefined, unchanged, document.hidden,
and poll-in-flight gate conditions
Addresses PR #6333 review feedback.
* feat(web-shell): call onSubmitBefore for queued prompts
Previously enqueuePrompt bypassed onSubmitBefore entirely. Now the
before hook is also invoked for queued prompts — if it rejects, the
prompt is cancelled and not added to the queue. The composer still
clears synchronously (fire-and-forget) since the Composer's onSubmit
contract is synchronous (boolean | void).
Also updates the onSubmitBefore JSDoc to reflect this behavior.
Addresses PR #6333 review feedback on security gap.
* test(web-shell): cover session callback behavior
* fix(web-shell): preserve rejected queued prompts
* fix(web-shell): preserve rejected direct prompts
---------
Co-authored-by: ytahdn <ytahdn@gmail.com>
* fix(scheduler): add opt-in per-tool-call execution timeout
Wrap each tool call in CoreToolScheduler with an optional execution timeout, disabled by default (experimental). Enable by setting QWEN_CODE_TOOL_EXECUTION_TIMEOUT_MS to a positive number of milliseconds.
On timeout the tool's derived AbortSignal is fired (cooperative tools stop; the shell kills its subprocess) and the call returns an EXECUTION_TIMEOUT ToolResult error, so a hung tool can no longer block the session indefinitely.
* fix(scheduler): address review feedback on tool execution timeout
- Use DOMException with name 'TimeoutError' instead of plain Error so
shell's getAbortReasonName diagnostic classifies timeouts correctly
- Move createToolTimeoutResult out of the constants block
- Add dedicated OTel constants (TOOL_FAILURE_KIND_TIMEOUT,
TOOL_SPAN_STATUS_TOOL_TIMEOUT) so timeouts are distinguishable from
generic tool errors in traces
- Add tests: parent signal abort forwarding, pre-aborted parent signal,
tool rejection propagation under active timeout
* fix(scheduler): address round-2 review feedback
- Fix createToolTimeoutResult displaying "0s" for timeouts < 500ms
(now shows ms: "500ms" instead of "0s")
- Fix forwardAbort listener leak when invocation.execute() throws
synchronously — move removeParentAbortForward cleanup to outer finally
- Add span assertion verifying tool.failure_kind === 'timeout' in
telemetry test
- Add completedCall.status assertion to parent abort test (documents
that parent abort takes precedence over tool rejection)
* fix(timeout): address review feedback on overflow, shell detection, and OTel spans
- Cap timeout at 2^31-1 to avoid setTimeout integer overflow
- Fix shell wasTimeout heuristic to detect scheduler timeouts via
TimeoutError abort reason (preserves partial output)
- Use TOOL_SPAN_STATUS_TOOL_TIMEOUT on exec sub-span for timeouts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Павел Каратаев <pavel.karataev@astrum.team>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
* docs: fix skill invocation syntax and include Feishu in channel lists
* docs: add Feishu column to channel media-handling table
Address review feedback on PR #6320: after adding Feishu to the prose
channel lists, the 'Platform differences' media-handling table still
omitted it. Add a Feishu column (images/files via the authenticated Open
API resources endpoint, 50MB limit; rich-text 'post' captions), verified
against packages/channels/feishu/src/media.ts and FeishuAdapter.ts. Add a
note that QQ Bot ignores incoming media (per QQChannel.ts) so it has no
row.
* docs: clarify Feishu post messages drop embedded images
The Platform differences table claimed Feishu rich-text (post) messages
carry 'mixed text + images', but FeishuAdapter's post parser only
extracts text/a/at nodes and silently drops img nodes (both the live
handler and the history-backfill path). Correct the Captions cell to
say text is extracted and embedded images are ignored.
* docs: mark clientId/clientSecret as required for Feishu too
Feishu declares requiredConfigFields: ['clientId', 'clientSecret']
(packages/channels/feishu/src/index.ts), same as DingTalk, but the
channel options table listed both fields as DingTalk-only. Update the
Required column and descriptions to cover Feishu (App ID / App Secret).
* docs: note token is not needed for Feishu in channel config table
* docs: note 50MB limit on Feishu image downloads
Feishu images and files both flow through the same downloadMedia() in
packages/channels/feishu/src/media.ts, which enforces a single
MAX_DOWNLOAD_BYTES = 50MB cap. Add the (50MB limit) note to the Images
cell for consistency with the Files cell.
* docs: clarify /skills panel-vs-run behavior per review feedback
- skills.md: add a migration Note that /skills <name> now opens the
Skills panel and ignores trailing args; use /<skill-name> to run.
- commands.md: list the /skills Usage cell as /skills, /<skill-name>
for consistency with the rest of the table.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 易良 <1204183885@qq.com>