feat(cli): add /reload-plugins command and plugin stale notification - #6037
feat(cli): add /reload-plugins command and plugin stale notification#6037ZijianZhang989 wants to merge 2 commits into
Conversation
|
Thanks for the PR! Template looks good ✓ — all required sections present, bilingual body, reviewer test plan included. On direction: this directly addresses #3696 (extension runtime refresh without restart) and mirrors a well-established Claude Code feature ( On approach: the two-slice design (manual command + stale flag) is the right granularity. The stale notification flow with idempotent Moving on to code review and testing. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必需章节齐全,双语正文,包含 reviewer 测试计划。 方向:直接解决 #3696(无需重启即可刷新扩展运行时),与 Claude Code 的成熟功能对齐( 方案:两个切片的设计(手动命令 + stale 标记)粒度恰当。幂等的 进入代码审查和测试 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal matched the PR's approach: a No correctness bugs, no security issues, no regressions found. A couple of observations (non-blocking):
TestsUnit tests (all PR-specific tests pass):
Build + typecheck: both pass clean ✅ Real-Scenario Testingtmux: command registrationtmux: interactive invocation check(The model correctly identifies Direct module test: plugin-refresh-state lifecycle中文说明代码审查独立提案与 PR 方案一致: 未发现正确性 bug、安全问题或回归。两个非阻塞观察:
测试单测(所有 PR 相关测试通过):
构建 + 类型检查: 均通过 ✅ 真实场景测试tmux:命令注册tmux:交互调用检查(模型正确识别 直接模块测试:plugin-refresh-state 生命周期— Qwen Code · qwen3.7-max |
|
Stepping back: this PR does exactly what it says. The motivation is real (#3696 — extension runtime refresh without process restart), the implementation mirrors Claude Code's proven approach, and every file in the diff serves the stated goal. The stale-flag-with-deferred-refresh design is the right call. It batches mutations cleanly, deduplicates notifications, and preserves backward compat for non-UI callers. The code is straightforward — no over-engineering, no speculative abstractions, no unnecessary indirection. The 6-parameter Tests confirm the lifecycle behavior (mark → dedup → clear → re-mark), the command registration, the reload summary formatting, and the failure-path semantics (failed reload doesn't clear the flag). Build and typecheck are clean. The pre-existing This ships the feature cleanly with good test coverage. ✅ 中文说明回顾:这个 PR 完全做到了它所承诺的。动机真实(#3696——无需重启进程即可刷新扩展运行时),实现镜像了 Claude Code 经过验证的方案,diff 中的每个文件都服务于声明的目标。 stale 标记 + 延迟刷新的设计是正确的选择。它干净地批量处理变更、去重通知、为非 UI 调用方保持向后兼容。代码直接——没有过度工程、没有投机性抽象、没有不必要的间接层。 6 参数的 测试确认了生命周期行为(标记 → 去重 → 清除 → 重新标记)、命令注册、重载摘要格式和失败路径语义(失败的重载不清除标记)。构建和类型检查均通过。预存的 功能干净交付,测试覆盖良好。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
d59a3c5 to
cfec8e6
Compare
更新:回退 disable/uninstall 的
|
| 操作 | refreshTools | markPluginsChanged |
|---|---|---|
| enable | false | ✅ |
| disable | false | ✅ |
| install | false | ✅ |
| uninstall | false | ✅ |
| update | false | ✅ |
| scope change | false | ✅ |
新增:模型可见 skills/agents 自动刷新
clearPluginCaches() 在每次变更后立即重建 SkillManager 和 SubagentManager 缓存,对齐 Claude Code 的 clearAllCaches() 模型层 memo 清理。
后续 PR
- Commands 和 hooks 尚未纳入自动刷新路径,pre-existing 架构缺口。
- 扩展文件系统 watcher(PR 后续部分)将检测扩展文件编辑并标记 stale。
d262d11 to
125f16d
Compare
125f16d to
b562ca0
Compare
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
[Suggestion] useExtensionUpdates.test.ts:279 — The beforeEach doesn't call resetPluginRefreshStateForTesting(). Since pluginRefreshNeeded is a module-level singleton, tests that trigger markPluginsChanged (via the auto-update path now wired in this PR) leave the flag set, polluting subsequent tests. Other test files (ExtensionsManagerDialog.test.tsx, extensionsCommand.test.ts) correctly add the reset — this file was missed.
Also noting two lower-priority items for consideration:
clearPluginCaches(hot-reload.ts:38-55) has no dedicated unit tests — the guard, happy path, and rejection branches are unverified.reloadPluginsRuntime(hot-reload.ts:74-93) has no timeout on its 5 sequential async steps. IfrestartMcpServershangs, the command blocks indefinitely.
— qwen3.7-max via Qwen Code /review
9ebd502 to
0b6fff1
Compare
0b6fff1 to
65134d6
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Add a /reload-plugins slash command and a plugin-stale notification flow
for extensions, aligning with Claude Code's plugin reload model.
When a user installs, uninstalls, updates, enables, disables, or changes
the scope of an extension, the runtime no longer rebuilds tool
registrations inline on every operation. Each mutation marks plugin
runtime state as stale and surfaces one deduplicated in-chat notice
directing the user to run /reload-plugins. The command performs one
coordinated refresh - extension cache, tools, plugin-provided LSP
servers, and slash commands - and reports a summary of what was reloaded.
A failed reload surfaces a friendly error and keeps the stale flag set so
the user can retry.
To let mutations defer the expensive tool refresh,
ExtensionManager.enableExtension / disableExtension / installExtension /
uninstallExtension / updateExtension gain an optional { refreshTools?: boolean }
option (default true, preserving existing behavior). All extension UI
entry points pass refreshTools: false so the refresh happens once inside
/reload-plugins instead of being interleaved with every mutation.
Model-visible skills and agents are auto-refreshed after every mutation
via clearPluginCaches(), which rebuilds SkillManager and SubagentManager
caches so the next model turn sees the updated active-extension set.
This matches Claude Code's clearAllCaches() for model-facing memoization.
Commands and hooks are not yet covered by automatic refresh.
Model-visible commands flow through CommandService, which has no
independent cache-invalidation primitive. Disabled-plugin hook pruning
requires a pruneRemovedPluginHooks equivalent. Both are tracked for a
follow-up PR.
Progress on QwenLM#3696.
ExtensionActionsView (4 calls), SourcesTab, and extensionsCommand all had clearPluginCaches inside outer try/catch blocks where a cache refresh failure would be misattributed to the wrapping mutation's error path. Wrap each call individually so cache errors don't cascade. Also export ExtensionRuntimeRefreshOptions so external TypeScript consumers can reference the type used in public method signatures.
65134d6 to
48ece8d
Compare
DragonnZhang
left a comment
There was a problem hiding this comment.
Independent Review Summary
Thorough review of 91 changed files (+5538/-824) across all subsystems touched by this PR:
Areas reviewed
-
Plugin reload command (
/reload-plugins,plugin-refresh-state.ts,hot-reload.ts): Stale flag lifecycle,clearPluginCacheserror isolation,reloadPluginsRuntimecall ordering, command registration. All previously flagged issues (||→&&guard, try/catch isolation forclearPluginCaches,ExtensionRuntimeRefreshOptionsexport) have been addressed. -
Workspace memory remember (
workspace-remember.ts,workspace-remember-errors.ts,remember.ts, bridgerunWorkspaceMemoryRemember/isWorkspaceMemoryRememberAvailable): Task lane serialization viathis.tail.then(run, run), content size validation (64KB limit), error code extraction chain,classifyTouchedScopespath escape detection, chat recording suppression viaAsyncLocalStorage. Implementation is sound. -
Channel daemon worker (
channel-worker-supervisor.ts,daemon-worker.ts,channel-worker-env.ts): Process lifecycle (spawn → ready message → SIGTERM → SIGKILL escalation), startup timeout withunref(), environment variable scrubbing, sentinel-based internal command gate, pidfile atomic operations (O_RDWR | O_NOFOLLOW+ ownership verification). Correctly handles concurrent startup races and graceful shutdown. -
Session loop detection (
Session.ts):recordDaemonInvalidToolParamsbucketing by tool name only,fillLoopSkippedFrom/fillPermissionSkippedFromfor batch cancellation, sequential-then-parallel execution when loop state is active,recordSkippedToolCallcallback propagation. TheLOOP_DETECTED_CONTEXT_MESSAGEis correctly appended to preserved tool runs. -
Bridge workspace control (
bridge.ts):hasNoChannelWorkwith spawn/restore/workspace-control in-flight counting,reapPendingEmptyChannelfor deferred teardown,withWorkspaceControlreference counting. Concurrency guards correctly prevent negative counts viaMath.max(0, ...). -
Mouse interactions (
useMouseEvents.ts,RowMouseController,TextInputMouseController): Reference-counted mouse mode tracking with level promotion (button→any), frame anchor calculation for bottom-anchored layouts, visual-to-logical click mapping with wide character support. -
SDK types (
types.ts,events.ts,acpRouteTable.ts,AcpWsTransport.ts): NewDaemonWorkspaceMemoryRemember*types,DaemonManagedMemoryChangedDatadiscriminated union, route table entries for/workspace/memory/remember,httpStatusextraction from JSON-RPC error data.
Verdict
No new high-confidence issues found. The 60 existing inline comments (from @qwen-code-ci-bot and @DragonnZhang) have been thoroughly addressed by the author. The security regression (disable/uninstall refreshTools: false → true) was correctly identified and fixed. The clearPluginCaches try/catch isolation pattern is now consistent across all 6 call sites.
The PR scope extends well beyond the title — it delivers workspace memory remember, channel daemon worker supervisor, session loop detection improvements, and mouse interactions in addition to the plugin reload command. Each subsystem is well-structured with appropriate error handling and test coverage.
qqqys
left a comment
There was a problem hiding this comment.
Rechecked latest head 48ece8d after the plugin cache/reload critical feedback. The remaining cache-refresh failure paths are isolated and the hook/runtime refresh path no longer has a critical blocker. No new critical issues found.
— GPT-5 via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed all 24 changed files. Three suggestions below — no blocking issues found. The plugin cache refresh and deferred /reload-plugins architecture is well-structured.
| if (!result) return; | ||
| if (config) { | ||
| try { | ||
| await clearPluginCaches(config); |
There was a problem hiding this comment.
[Suggestion] When N auto-updates fire concurrently (e.g. batch update check finds N extensions), each .then() handler calls clearPluginCaches(config) independently. Since clearPluginCaches walks the filesystem to refresh skill/subagent caches, this means N redundant walks for the same result.
Consider debouncing: use a module-level pendingCacheRefresh promise so concurrent callers share a single refresh:
let pendingCacheRefresh: Promise<void> | null = null;
function debouncedClearPluginCaches(config: Config) {
return pendingCacheRefresh ??= clearPluginCaches(config).finally(() => {
pendingCacheRefresh = null;
});
}Alternatively, batch the cache clear to fire once after all concurrent updates settle.
— qwen3.7-max via Qwen Code /review
| expect(frame).toContain('Extension v1.0.0'); | ||
| }); | ||
|
|
||
| it('marks plugins stale when toggling an installed plugin', async () => { |
There was a problem hiding this comment.
[Suggestion] This test covers the disable path (toggle ON → OFF, asserts disableExtension called) but not the enable path (toggle OFF → ON, asserts enableExtension called). The toggle flow branches on currentState.enabled with different extensionManager methods on each side. A second test case that starts with an enabled extension and toggles it off would cover the other branch and ensure enableExtension is called with the same { refreshTools: false } pattern.
— qwen3.7-max via Qwen Code /review
| @@ -289,9 +294,19 @@ export const useExtensionUpdates = ( | |||
| payload: { name: extensionName, state }, | |||
| }); | |||
| }, | |||
There was a problem hiding this comment.
[Suggestion] The bare false fourth argument here (and at ExtensionActionsView.tsx:215) is not self-documenting. Sibling methods in this PR (installExtension, disableExtension, enableExtension, uninstallExtension) all use the named options bag { refreshTools: false }, but updateExtension still takes a positional boolean. Aligning updateExtension's signature to accept ExtensionRuntimeRefreshOptions — or at least passing { refreshTools: false } and extracting the boolean inside — would make the intent clear at call sites without requiring readers to check the method signature.
— qwen3.7-max via Qwen Code /review
🔬 Local verification report (maintainer)Built the real binary at PR head Verdict: the stated behavior works and is well‑covered. One code‑vs‑review‑reply discrepancy and two intentional tradeoffs are worth a look before merge; everything else is non‑blocking. Environment
✅ Unit tests — all green
🧪 Mutation testing — core invariants are load‑bearingIntroduced 4 targeted mutations; each made the intended test fail (then reverted), proving the tests actually hold the behavior:
🖥️ Real TUI E2E (tmux + real binary, isolated
|
| # | 变异 | 抓到它的测试 | 失败信息 |
|---|---|---|---|
| 1 | 去掉 markPluginsChanged 的幂等 guard |
deduplicates plugin refresh notifications |
expected true to be false |
| 2 | 在 catch(失败路径)里清 stale flag |
surfaces reload failures without clearing the refresh flag |
clearPluginsChanged 被调用 1 次(应为 0 次) |
| 3 | enableExtension 的 ?? true → ?? false |
defaults to refreshing tools when options are omitted |
refreshTools 调用 0 次(应为 1 次) |
| 4 | 删掉 reloadPluginsRuntime 里的 refreshTools() |
两个 reloadPluginsRuntime 用例 |
refreshTools 未被调用 |
🖥️ 真实 TUI 端到端(tmux + 真实二进制,隔离 QWEN_HOME,2 个测试扩展)
/reload-plugins已注册并可运行 →Reloaded: 2 plugins · 0 commands · 0 skills · 0 hooks · 0 plugin MCP servers · 0 plugin LSP servers——计数反映 2 个 active 扩展(非硬编码)。- 在
/extensions manage(Installed 标签页)toggle 一个扩展 → 聊天出现Plugin changes detected. Run /reload-plugins to apply them. - 去重: 不重载再 toggle 第二次,没有重复通知(总计 1 条)。
- 清除生命周期:
/reload-plugins清 flag 后,下一次 toggle 重新通知(总计 2 条)——完整的markPluginsChanged/clearPluginsChanged生命周期在真实 app 里得到确认。 - 每次 toggle 内联执行
clearPluginCaches无崩溃(toggle 成功 + 通知触发)。
(「失败重载保留 flag」的路径未在 TUI 里强制触发,但已由承重的单元测试覆盖——即上面的变异 #2。)
⚠️ 合并前值得权衡(是权衡,不是缺陷)
- disable/uninstall 用
refreshTools: false。 在 head48ece8de,所有 disable/uninstall 的 UI 调用都传{ refreshTools: false }(InstalledTab.tsx:467、ExtensionActionsView.tsx:119 / 257 / 323)。效果:disable/uninstall 时clearPluginCaches立即刷新 skills + subagents,但 MCP server 拆除(restartMcpServers)、hierarchical memory(context)、工具注册被延迟到/reload-plugins。这正是 bot 最初的 [Critical](被移除扩展的 MCP server 会一直运行到重载)。PR 把它定性为已知的 pre‑existing gap(缺pruneRemovedPluginHooks等价物)并留 follow‑up——这个取舍合理,但对依赖「立即拆除」的场景是真实的行为变化。- 🔺 需确认的矛盾: 已 RESOLVED 的评审回复写道 「已把 disable/uninstall 改为默认
refreshTools: true,使扩展的 MCP servers、skills、agents、context 立即拆除」。而当前代码仍是refreshTools: false,因此 MCP servers + context 并未立即拆除(只有 skills + subagents 立即刷新)。请确认refreshTools: false是否为最终意图。
- 🔺 需确认的矛盾: 已 RESOLVED 的评审回复写道 「已把 disable/uninstall 改为默认
/extensions install不再调用reloadCommands()。 新装扩展的斜杠命令现在要/reload-plugins后才出现。PR 已说明;仅为 UX 提示。
📝 未解决的非阻塞 [Suggestion](最新 bot 评审——均成立)
- 并发 auto‑update 各自调用
clearPluginCaches→ N 次冗余文件系统遍历(可 debounce)。 ExtensionsManagerDialog.test.tsx只覆盖 disable 分支,未覆盖 enable。updateExtension仍用位置boolean,而同类方法都用{ refreshTools }——不够自解释。
🔎 次要
clearPluginCaches的 guard 用&&:if (!getSkillManager && !getSubagentManager) return。若某个Config恰好只有两个 getter 中的一个,Promise.allSettled([...])数组构造会抛TypeError(optional chaining 只保护?.refreshCache,不保护getXxxManager()调用本身)。真实Config两个 getter 都有、且调用方都用 try/catch 包裹,故不可达——仅为完整性记录。clearPluginCaches无专门单测(bot 已指出);E2E 间接覆盖了它。
CI
Test (ubuntu-latest)——pass(跑 lint / typecheck / i18n 静态检查的那条 leg)。reviewDecision: CHANGES_REQUESTED是早期 bot 评审的残留;最新 bot 评审说「no blocking issues」,qqqys(GPT‑5) 在48ece8de已 APPROVED。
总结: 功能正确且测试充分。建议合并前确认 disable/uninstall 的 refreshTools: false 是否为最终意图(对照那条 RESOLVED 回复);其余均非阻塞。
|
Thanks everyone for the feedback and discussions here. I'm closing this PR and moving the work to a new one #6152. Please follow the new PR for further updates. |
What this PR does
This PR adds a
/reload-pluginsslash command and a lightweight plugin-stale notification flow for Qwen Code extensions, aligning with Claude Code's plugin reload model. When a user installs, uninstalls, updates, enables, disables, or changes the scope of an extension, the runtime no longer rebuilds tool registrations inline on every operation. Instead each mutation marks plugin runtime state as stale and surfaces one deduplicated in-chat notice:Plugin changes detected. Run /reload-plugins to apply them.Running/reload-pluginsperforms one coordinated refresh — extension cache, tools, plugin-provided LSP servers, and slash commands — and reports a summary (plugins, commands, skills, hooks, MCP/LSP server counts). A failed reload surfaces a friendly error and keeps the stale flag set so the user can retry.To let mutations defer the expensive tool refresh,
ExtensionManager.enableExtension/disableExtension/installExtension/uninstallExtension/updateExtensiongain an optional{ refreshTools?: boolean }option (defaulttrue, preserving existing behavior). All extension UI entry points passrefreshTools: falseso the refresh happens once, inside/reload-plugins, instead of being interleaved with every mutation.Model-visible skills and agents are auto-refreshed after every mutation via
clearPluginCaches(), which rebuildsSkillManagerandSubagentManagercaches so the next model turn sees the updated active-extension set — matching Claude Code'sclearAllCaches()for model-facing memoization.Why it's needed
Issue #3696 asks for extension-provided runtime features to be refreshable without a full process restart. The design aligns with Claude Code's plugin reload model: a narrow, manual, plugin-scoped reload command, with automatic hot reload left to the subsystems that already support it.
/reload-pluginsonly touches extension runtime state, and only extension management mutations mark plugins stale.Development process
This PR delivers the first two slices together, because the manual command is only useful once mutations route through the stale flag.
Slice 1 — Manual reload command. Added
reloadPluginsRuntime()with the runtime sequence:refreshCache()→refreshTools()→ optional LSP reinitialize → optional slash command reload. LSP reinitialize is duck-typed (config.reinitializeLsp?.()) because the CLI layer does not yet have a stable LSP service type; this cast can be removed once the LSP API stabilizes. Addedreload-plugins-command.tsand registered it inBuiltinCommandLoader. AddedReloadPluginsSummaryso the command can report what was reloaded, matching Claude Code'sReloaded: N plugins · N commands · ...shape.Slice 2 — Plugin stale state and mutation wiring. Added
plugin-refresh-state.tswithmarkPluginsChanged(reason),clearPluginsChanged(),needsPluginRefresh().markPluginsChangedis idempotent — repeated stale events before reload do not spam notifications.AppContainersubscribes to thePluginRefreshNeededevent and pushes one in-chat history item. Wired every extension management mutation tomarkPluginsChanged: install (CLI + Discover + Sources), enable/disable (Installed + Actions), update (Actions +useExtensionUpdates), scope change (Actions), uninstall (Actions). On the core side, added{ refreshTools?: boolean }to the relevantExtensionManagermethods so mutations skip the inline tool refresh and defer it to/reload-plugins.clearPluginsChanged()runs only after a successful reload — a failed reload leaves the flag set.Model-visible skills/agents auto-refresh. Added
clearPluginCaches()that rebuildsSkillManagerandSubagentManagercaches immediately after every mutation, so the next model turn sees the updated active-extension set without waiting for/reload-plugins. This matches Claude Code'sclearAllCaches()memo clearing.What's next. The extension filesystem watcher is out of scope for this PR and will follow. It will detect extension runtime file edits the user did not initiate —
qwen-extension.jsonmanifest edits, extension command/agent/hook files, and extension storage files — and mark plugins stale instead of auto-refreshing. Existing subsystem-owned hot reload (skills viaSkillManager, settings-backed MCP, LSP config) is intentionally left untouched and will not be duplicated.Commands and hooks are not yet covered by the auto-refresh path. Model-visible commands flow through
CommandService, which has no independent cache-invalidation primitive. Disabled-plugin hook pruning requires apruneRemovedPluginHooksequivalent that qwen-code does not yet have. Both will be addressed in a follow-up PR.Reviewer Test Plan
How to verify
node packages/cli/dist/index.js. Use a cleanQWEN_HOME(e.g.QWEN_HOME=/tmp/qwen-plugin-reload-home) if you want toggle state to actually flip./extensions→ Installed tab → space to toggle an extension.Plugin changes detected. Run /reload-plugins to apply them./reload-plugins. Expected:Reloaded: N plugins · N commands · N skills · N hooks · N plugin MCP servers · N plugin LSP servers./reload-plugins. Expected: anerrormessageReload failed: ..., and the stale flag is NOT cleared — re-running after the cause is fixed still works.Unit tests cover: stale flag lifecycle and dedup (
plugin-refresh-state.test.ts); the/reload-pluginscommand success and failure paths, including "failure does not clear the flag" (reload-plugins-command.test.ts);reloadPluginsRuntimecall ordering and optional LSP (hot-reload.test.ts); command registration (BuiltinCommandLoader.test.ts); install no longer auto-reloading (extensionsCommand.test.ts); toggle marking stale (ExtensionsManagerDialog.test.tsx); the corerefreshToolsoption (extensionManager.test.ts).Evidence (Before & After)
N/A — behavior is observable via the in-chat notification and
/reload-pluginssummary; see steps above._2026-06-30.112838.mp4
Tested on
Environment (optional)
Local macOS development,
node packages/cli/dist/index.js; unit tests vianpx vitest runinpackages/cliandpackages/core.Risk & Scope
/reload-pluginsto activate changes. This is intentional and matches Claude Code, but anyone relying on the old inlinerefreshToolsper operation will see a one-step delay. TherefreshToolsoption defaults totrue, so non-UI callers and programmatic paths are unaffected..lsp.jsonhot reload, andSkillManagerskill content watching remain subsystem-owned and are not touched. AneedsRefreshflag for non-extension subsystems is intentionally not added. Model-visible command cache invalidation and disabled-plugin hook pruning are tracked for a follow-up PR.options?parameter onExtensionManagermethods is optional and defaults to current behavior.Linked Issues
Progress on #3696 (sub-task 5: reload slash command; sub-task 6: needsRefresh notification, scoped to the extension/plugin subsystem).
中文说明
这个 PR 做了什么
本 PR 为 Qwen Code 扩展新增
/reload-plugins斜杠命令和一套轻量的"插件需要重载"通知流程,对齐 Claude Code 的插件重载模型。用户安装、卸载、更新、启用、禁用或修改扩展作用域时,运行时不再在每次操作后内联重建工具注册;改为将插件运行时状态标记为 stale,并在聊天中推送一条去重后的提示:Plugin changes detected. Run /reload-plugins to apply them.。执行/reload-plugins会一次性完成协调刷新——扩展缓存、工具、插件提供的 LSP server、斜杠命令——并报告重载摘要(插件、命令、skill、hook、MCP/LSP server 数量)。重载失败时返回友好错误信息,并保留 stale 标记,用户可重试。为让变更延迟执行昂贵的 tool 刷新,
ExtensionManager的enableExtension/disableExtension/installExtension/uninstallExtension/updateExtension新增可选参数{ refreshTools?: boolean }(默认true,保持现有行为)。所有扩展 UI 入口传refreshTools: false,让刷新集中在/reload-plugins一次完成,而不是穿插在每次变更中。模型可见的 skills 和 agents 在每次变更后通过
clearPluginCaches()立即自动刷新——重建SkillManager和SubagentManager缓存,模型下一轮就能看到更新后的活跃扩展集合。这对应 Claude Code 的clearAllCaches()模型层 memo 清理。为什么需要
Issue #3696 要求扩展提供的运行时特性无需重启进程即可刷新。设计对齐 Claude Code 的插件重载模型:一个范围窄、手动触发、只针对插件运行时的重载命令,自动热重载留给已经支持它的子系统。
/reload-plugins只动扩展运行时状态,只有扩展管理操作会标记插件为 stale。开发过程
本 PR 一次性交付前两个切片,因为手动命令只有在变更路由到 stale 标记后才有意义。
切片 1 —— 手动重载命令。 新增
reloadPluginsRuntime(),运行时顺序为:refreshCache()→refreshTools()→ 可选的 LSP 重初始化 → 可选的斜杠命令重载。LSP 重初始化采用 duck typing,因为 CLI 层还没有稳定的 LSP service 类型;等 LSP API 稳定后可移除。新增reload-plugins-command.ts并在BuiltinCommandLoader中注册。新增ReloadPluginsSummary,让命令能报告重载了什么,与 Claude Code 的Reloaded: N plugins · N commands · ...形式一致。切片 2 —— 插件 stale 状态与变更接线。 新增
plugin-refresh-state.ts,提供markPluginsChanged(reason)、clearPluginsChanged()、needsPluginRefresh()。markPluginsChanged是幂等的——重载前的重复 stale 事件不会刷屏。AppContainer订阅PluginRefreshNeeded事件,向聊天历史推送一条消息。将所有扩展管理操作接线到markPluginsChanged:install、enable/disable、update、scope 变更、uninstall。在 core 层,给相关ExtensionManager方法新增{ refreshTools?: boolean },让变更跳过内联 tool 刷新,延迟到/reload-plugins。clearPluginsChanged()只在重载成功后执行——重载失败时保留 stale 标记。模型可见 skills/agents 自动刷新。 新增
clearPluginCaches(),在每次扩展变更后立即重建SkillManager和SubagentManager缓存,模型下一轮无需等待/reload-plugins即可看到更新后的活跃扩展集合。对齐 Claude Code 的clearAllCaches()memo 清理。后续计划。 扩展文件系统 watcher 不在本 PR 范围内,后续补上。它将检测用户未主动发起的扩展运行时文件编辑——
qwen-extension.jsonmanifest 编辑、扩展 command/agent/hook 文件、扩展存储文件——并标记插件为 stale 而非自动刷新。已有子系统级热重载(skills 经SkillManager、settings 支持的 MCP、LSP config)保持不动。Commands 和 hooks 尚未纳入自动刷新路径。模型可见的 commands 通过
CommandService流转,该服务目前没有独立的缓存失效原语。被禁用插件的 hooks 立即摘除需要pruneRemovedPluginHooks等价实现,qwen-code 目前缺失。两者将在后续 PR 中处理。Reviewer 测试计划
如何验证
node packages/cli/dist/index.js。若希望 toggle 状态真正翻转,使用干净的QWEN_HOME(如QWEN_HOME=/tmp/qwen-plugin-reload-home)。/extensions→ Installed 标签页 → 空格 toggle 一个扩展。Plugin changes detected. Run /reload-plugins to apply them./reload-plugins。预期:返回Reloaded: N plugins · N commands · N skills · N hooks · N plugin MCP servers · N plugin LSP servers。/reload-plugins。预期:返回error消息Reload failed: ...,且 stale 标记未清除——修复后重跑仍可生效。单测覆盖:stale 标记生命周期与去重、
/reload-plugins命令成功与失败路径、reloadPluginsRuntime调用顺序与可选 LSP、命令注册、install 不再自动刷新、toggle 设置 stale、core 层refreshTools选项。测试环境
风险与范围
/reload-plugins激活。这是有意的,与 Claude Code 一致,但依赖旧的内联refreshTools的路径会有一步延迟。refreshTools选项默认true,非 UI 调用方和编程式路径不受影响。.lsp.json热重载、SkillManager的 skill 内容监听仍由子系统拥有。非扩展子系统的needsRefresh标记有意不加。模型可见的 command 缓存失效和被禁用插件的 hooks 立即摘除留待后续 PR。ExtensionManager方法新增的options?参数可选,默认行为不变。关联 Issue
推进 https://github.com/QwenLM/qwen-code/issues/3696(子任务 5:reload 斜杠命令;子任务 6:needsRefresh 通知,范围限定在扩展/插件子系统)。