Skip to content

feat(cli): add /reload-plugins command and plugin stale notification - #6037

Closed
ZijianZhang989 wants to merge 2 commits into
QwenLM:mainfrom
ZijianZhang989:feat/settings-refresh-classification
Closed

feat(cli): add /reload-plugins command and plugin stale notification#6037
ZijianZhang989 wants to merge 2 commits into
QwenLM:mainfrom
ZijianZhang989:feat/settings-refresh-classification

Conversation

@ZijianZhang989

@ZijianZhang989 ZijianZhang989 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a /reload-plugins slash 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-plugins performs 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 / 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 — matching Claude Code's clearAllCaches() 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-plugins only 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. Added reload-plugins-command.ts and registered it in BuiltinCommandLoader. Added ReloadPluginsSummary so the command can report what was reloaded, matching Claude Code's Reloaded: N plugins · N commands · ... shape.

Slice 2 — Plugin stale state and mutation wiring. Added plugin-refresh-state.ts with markPluginsChanged(reason), clearPluginsChanged(), needsPluginRefresh(). markPluginsChanged is idempotent — repeated stale events before reload do not spam notifications. AppContainer subscribes to the PluginRefreshNeeded event and pushes one in-chat history item. Wired every extension management mutation to markPluginsChanged: 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 relevant ExtensionManager methods 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 rebuilds SkillManager and SubagentManager caches immediately after every mutation, so the next model turn sees the updated active-extension set without waiting for /reload-plugins. This matches Claude Code's clearAllCaches() 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.json manifest edits, extension command/agent/hook files, and extension storage files — and mark plugins stale instead of auto-refreshing. Existing subsystem-owned hot reload (skills via SkillManager, 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 a pruneRemovedPluginHooks equivalent that qwen-code does not yet have. Both will be addressed in a follow-up PR.

Reviewer Test Plan

How to verify

  1. Start an interactive session: node packages/cli/dist/index.js. Use a clean QWEN_HOME (e.g. QWEN_HOME=/tmp/qwen-plugin-reload-home) if you want toggle state to actually flip.
  2. /extensionsInstalled tab → space to toggle an extension.
  3. Exit the dialog. Expected: chat history shows Plugin changes detected. Run /reload-plugins to apply them.
  4. Run /reload-plugins. Expected: Reloaded: N plugins · N commands · N skills · N hooks · N plugin MCP servers · N plugin LSP servers.
  5. Toggle again without reloading. Expected: no duplicate notification (stale flag dedupes until cleared).
  6. (Failure path) Force a reload failure and run /reload-plugins. Expected: an error message Reload 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-plugins command success and failure paths, including "failure does not clear the flag" (reload-plugins-command.test.ts); reloadPluginsRuntime call 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 core refreshTools option (extensionManager.test.ts).

Evidence (Before & After)

N/A — behavior is observable via the in-chat notification and /reload-plugins summary; see steps above.

_2026-06-30.112838.mp4

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local macOS development, node packages/cli/dist/index.js; unit tests via npx vitest run in packages/cli and packages/core.

Risk & Scope

  • Main risk or tradeoff: extension mutations no longer apply immediately — the user must run /reload-plugins to activate changes. This is intentional and matches Claude Code, but anyone relying on the old inline refreshTools per operation will see a one-step delay. The refreshTools option defaults to true, so non-UI callers and programmatic paths are unaffected.
  • Not validated / out of scope: the extension filesystem watcher and boundary tests for hooks/channels/context files. Settings.json-driven MCP/skill hot reload, LSP .lsp.json hot reload, and SkillManager skill content watching remain subsystem-owned and are not touched. A needsRefresh flag 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.
  • Breaking changes / migration notes: none. The new options? parameter on ExtensionManager methods 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 刷新,ExtensionManagerenableExtension / disableExtension / installExtension / uninstallExtension / updateExtension 新增可选参数 { refreshTools?: boolean }(默认 true,保持现有行为)。所有扩展 UI 入口传 refreshTools: false,让刷新集中在 /reload-plugins 一次完成,而不是穿插在每次变更中。

模型可见的 skills 和 agents 在每次变更后通过 clearPluginCaches() 立即自动刷新——重建 SkillManagerSubagentManager 缓存,模型下一轮就能看到更新后的活跃扩展集合。这对应 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-pluginsclearPluginsChanged() 只在重载成功后执行——重载失败时保留 stale 标记。

模型可见 skills/agents 自动刷新。 新增 clearPluginCaches(),在每次扩展变更后立即重建 SkillManagerSubagentManager 缓存,模型下一轮无需等待 /reload-plugins 即可看到更新后的活跃扩展集合。对齐 Claude Code 的 clearAllCaches() memo 清理。

后续计划。 扩展文件系统 watcher 不在本 PR 范围内,后续补上。它将检测用户未主动发起的扩展运行时文件编辑——qwen-extension.json manifest 编辑、扩展 command/agent/hook 文件、扩展存储文件——并标记插件为 stale 而非自动刷新。已有子系统级热重载(skills 经 SkillManager、settings 支持的 MCP、LSP config)保持不动。

Commands 和 hooks 尚未纳入自动刷新路径。模型可见的 commands 通过 CommandService 流转,该服务目前没有独立的缓存失效原语。被禁用插件的 hooks 立即摘除需要 pruneRemovedPluginHooks 等价实现,qwen-code 目前缺失。两者将在后续 PR 中处理。

Reviewer 测试计划

如何验证

  1. 启动交互 session:node packages/cli/dist/index.js。若希望 toggle 状态真正翻转,使用干净的 QWEN_HOME(如 QWEN_HOME=/tmp/qwen-plugin-reload-home)。
  2. /extensionsInstalled 标签页 → 空格 toggle 一个扩展。
  3. 退出对话框。预期:聊天历史出现 Plugin changes detected. Run /reload-plugins to apply them.
  4. 执行 /reload-plugins。预期:返回 Reloaded: N plugins · N commands · N skills · N hooks · N plugin MCP servers · N plugin LSP servers
  5. 不重载再次 toggle。预期:不重复提示(stale 标记幂等,直到清除)。
  6. (失败路径)制造重载失败并执行 /reload-plugins。预期:返回 error 消息 Reload failed: ...,且 stale 标记未清除——修复后重跑仍可生效。

单测覆盖:stale 标记生命周期与去重、/reload-plugins 命令成功与失败路径、reloadPluginsRuntime 调用顺序与可选 LSP、命令注册、install 不再自动刷新、toggle 设置 stale、core 层 refreshTools 选项。

测试环境

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

风险与范围

  • 主要风险/权衡:扩展变更不再立即生效——用户需执行 /reload-plugins 激活。这是有意的,与 Claude Code 一致,但依赖旧的内联 refreshTools 的路径会有一步延迟。refreshTools 选项默认 true,非 UI 调用方和编程式路径不受影响。
  • 未验证/范围外:扩展文件系统 watcher 以及 hooks/channels/context files 的边界测试。settings.json 驱动的 MCP/skill 热重载、LSP .lsp.json 热重载、SkillManager 的 skill 内容监听仍由子系统拥有。非扩展子系统的 needsRefresh 标记有意不加。模型可见的 command 缓存失效和被禁用插件的 hooks 立即摘除留待后续 PR。
  • 破坏性变更/迁移说明:无。ExtensionManager 方法新增的 options? 参数可选,默认行为不变。

关联 Issue

推进 https://github.com/QwenLM/qwen-code/issues/3696(子任务 5:reload 斜杠命令;子任务 6:needsRefresh 通知,范围限定在扩展/插件子系统)。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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 (/reload-plugins appears across multiple CHANGELOG entries — plugin reload, dependency auto-install, remote control support). The scope is narrow — extension runtime only — and doesn't touch core systems. Clearly aligned.

On approach: the two-slice design (manual command + stale flag) is the right granularity. The stale notification flow with idempotent markPluginsChanged + deferred refreshTools is a clean way to batch mutations without losing correctness. The { refreshTools?: boolean } option defaulting to true preserves backward compat for non-UI callers. Every changed file maps directly to the stated goal — no drive-by refactors or scope creep.

Moving on to code review and testing. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必需章节齐全,双语正文,包含 reviewer 测试计划。

方向:直接解决 #3696(无需重启即可刷新扩展运行时),与 Claude Code 的成熟功能对齐(/reload-plugins 在多个 CHANGELOG 条目中出现——插件重载、依赖自动安装、远程控制支持)。范围窄——仅限扩展运行时——不涉及核心系统。方向明确对齐。

方案:两个切片的设计(手动命令 + stale 标记)粒度恰当。幂等的 markPluginsChanged + 延迟 refreshTools 的 stale 通知流程,在不丢失正确性的前提下干净地批量处理变更。{ refreshTools?: boolean } 选项默认 true,对非 UI 调用方保持向后兼容。每个变更文件都直接对应声明的目标——没有顺手重构或范围蔓延。

进入代码审查和测试 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal matched the PR's approach: a /reload-plugins slash command + module-level stale flag + deferred refreshTools option on ExtensionManager methods. The PR's implementation is clean and matches this design exactly.

No correctness bugs, no security issues, no regressions found. A couple of observations (non-blocking):

  • installExtension now has 6 parameters — the trailing { refreshTools: false } after four undefined values is noisy at call sites. Not a blocker, but a future refactor to an options object would clean this up.
  • The ConfigWithOptionalLspReload duck typing is pragmatic given the unstable LSP API. The cast is well-scoped and can be removed when the LSP type stabilizes.
  • plugin-refresh-state.ts is minimal (30 lines) and idempotent — exactly the right size for the job.

Tests

Unit tests (all PR-specific tests pass):

Test file Result
plugin-refresh-state.test.ts 1/1 ✅
reload-plugins-command.test.ts 3/3 ✅
hot-reload.test.ts 26/26 ✅
BuiltinCommandLoader.test.ts 11/11 ✅
extensionsCommand.test.ts 11/11 ✅
extensionManager.test.ts 22/56 (34 pre-existing env failures, unrelated to PR)

Build + typecheck: both pass clean ✅

Real-Scenario Testing

tmux: command registration

$ QWEN_HOME=/tmp/qwen-triage-home node dist/cli.js -p 'list all available slash commands that contain reload in their name'

Based on the codebase search, there is **1** slash command containing "reload" in its name:

- **`/reload-plugins`** — Reloads extension runtime changes (plugins, commands, skills, hooks, MCP servers, LSP servers).
  Defined in `packages/cli/src/ui/commands/reload-plugins-command.ts`.

tmux: interactive invocation check

$ QWEN_HOME=/tmp/qwen-triage-home node dist/cli.js -p 'Run /reload-plugins and show the output'

`/reload-plugins` is a built-in CLI slash command handled by the Qwen Code runtime — I can't invoke it from
within a session myself. You can type `/reload-plugins` directly in your CLI prompt and it will reload any
stale plugins and show the result.

(The model correctly identifies /reload-plugins as a registered built-in slash command. Full interactive testing with toggling extensions requires a real user session and installed extensions — see the Reviewer Test Plan in the PR body.)

Direct module test: plugin-refresh-state lifecycle

$ node -e "import('./packages/cli/dist/src/config/plugin-refresh-state.js').then(m => { ... })"

Initial needsPluginRefresh: false
markPluginsChanged (first): true
needsPluginRefresh: true
markPluginsChanged (second, should dedup): false
After clear, needsPluginRefresh: false
markPluginsChanged (after clear): true
All checks passed ✅
中文说明

代码审查

独立提案与 PR 方案一致:/reload-plugins 斜杠命令 + 模块级 stale 标记 + ExtensionManager 方法上延迟的 refreshTools 选项。PR 实现干净,与设计完全匹配。

未发现正确性 bug、安全问题或回归。两个非阻塞观察:

  • installExtension 现在有 6 个参数——四个 undefined 之后跟 { refreshTools: false } 在调用点有些冗余。不是阻塞项,但未来重构为 options 对象会更干净。
  • ConfigWithOptionalLspReload 的 duck typing 在 LSP API 不稳定的前提下是务实的。等 LSP 类型稳定后可移除。
  • plugin-refresh-state.ts 精简(30 行)且幂等——恰好完成所需的工作。

测试

单测(所有 PR 相关测试通过):

测试文件 结果
plugin-refresh-state.test.ts 1/1 ✅
reload-plugins-command.test.ts 3/3 ✅
hot-reload.test.ts 26/26 ✅
BuiltinCommandLoader.test.ts 11/11 ✅
extensionsCommand.test.ts 11/11 ✅
extensionManager.test.ts 22/56(34 个预存环境故障,与 PR 无关)

构建 + 类型检查: 均通过 ✅

真实场景测试

tmux:命令注册

$ QWEN_HOME=/tmp/qwen-triage-home node dist/cli.js -p 'list all available slash commands that contain reload in their name'

Based on the codebase search, there is **1** slash command containing "reload" in its name:

- **`/reload-plugins`** — Reloads extension runtime changes (plugins, commands, skills, hooks, MCP servers, LSP servers).
  Defined in `packages/cli/src/ui/commands/reload-plugins-command.ts`.

tmux:交互调用检查

$ QWEN_HOME=/tmp/qwen-triage-home node dist/cli.js -p 'Run /reload-plugins and show the output'

`/reload-plugins` is a built-in CLI slash command handled by the Qwen Code runtime — I can't invoke it from
within a session myself. You can type `/reload-plugins` directly in your CLI prompt and it will reload any
stale plugins and show the result.

(模型正确识别 /reload-plugins 为已注册的内置斜杠命令。包含 toggle 扩展的完整交互测试需要真实用户 session 和已安装扩展——见 PR 正文中的 Reviewer 测试计划。)

直接模块测试:plugin-refresh-state 生命周期

$ node -e "import('./packages/cli/dist/src/config/plugin-refresh-state.js').then(m => { ... })"

Initial needsPluginRefresh: false
markPluginsChanged (first): true
needsPluginRefresh: true
markPluginsChanged (second, should dedup): false
After clear, needsPluginRefresh: false
markPluginsChanged (after clear): true
All checks passed ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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. plugin-refresh-state.ts at 30 lines is the ideal size.

The 6-parameter installExtension call sites are a bit noisy with trailing undefined values, but that's a pre-existing signature design issue, not something this PR introduced. The duck-typed LSP reload is pragmatic and well-scoped.

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 extensionManager.test.ts environmental failures are unrelated to this change.

This ships the feature cleanly with good test coverage. ✅

中文说明

回顾:这个 PR 完全做到了它所承诺的。动机真实(#3696——无需重启进程即可刷新扩展运行时),实现镜像了 Claude Code 经过验证的方案,diff 中的每个文件都服务于声明的目标。

stale 标记 + 延迟刷新的设计是正确的选择。它干净地批量处理变更、去重通知、为非 UI 调用方保持向后兼容。代码直接——没有过度工程、没有投机性抽象、没有不必要的间接层。plugin-refresh-state.ts 30 行是理想的大小。

6 参数的 installExtension 调用点因尾部 undefined 值有些冗余,但那是预先存在的签名设计问题,不是本 PR 引入的。duck typing 的 LSP 重载务实且范围明确。

测试确认了生命周期行为(标记 → 去重 → 清除 → 重新标记)、命令注册、重载摘要格式和失败路径语义(失败的重载不清除标记)。构建和类型检查均通过。预存的 extensionManager.test.ts 环境故障与本变更无关。

功能干净交付,测试覆盖良好。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot added category/cli Command line interface and interaction scope/extensions Extension configuration status/in-review This issue is currently in review. type/feature-request New feature or enhancement request labels Jun 30, 2026
Comment thread packages/core/src/extension/extensionManager.test.ts
Comment thread packages/cli/src/ui/commands/extensionsCommand.ts
Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch 4 times, most recently from d59a3c5 to cfec8e6 Compare June 30, 2026 08:52
@ZijianZhang989

ZijianZhang989 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

更新:回退 disable/uninstall 的 refreshTools: truefalse

经过对 Claude Code 源码的深入验证,之前关于"禁用/卸载扩展应立即断开 MCP 连接"的安全顾虑不成立。Claude Code 的 disablePluginOp / uninstallPluginOp 只调用 clearAllCaches()(清 memo 缓存 + prune 被移除插件的 hooks),完全不触碰 MCP 进程——MCP 连接保持运行,等待 /reload-plugins。在 /plugin UI 中 MCP server 看起来"即时生效"是因为它走的是独立的 MCP 专用路径 toggleMcpServer,而非插件 enable/disable 生命周期。

当前状态:所有扩展变更统一延迟到 /reload-plugins

所有七个扩展操作都走 refreshTools: false + markPluginsChanged,与 Claude Code 完全一致:

操作 refreshTools markPluginsChanged
enable false
disable false
install false
uninstall false
update false
scope change false

新增:模型可见 skills/agents 自动刷新

clearPluginCaches() 在每次变更后立即重建 SkillManagerSubagentManager 缓存,对齐 Claude Code 的 clearAllCaches() 模型层 memo 清理。

后续 PR

  • Commands 和 hooks 尚未纳入自动刷新路径,pre-existing 架构缺口。
  • 扩展文件系统 watcher(PR 后续部分)将检测扩展文件编辑并标记 stale。

@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch 3 times, most recently from d262d11 to 125f16d Compare June 30, 2026 09:24
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts
Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/cli/src/config/hot-reload.ts Outdated
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts Outdated
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts Outdated
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch from 125f16d to b562ca0 Compare June 30, 2026 11:23

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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. If restartMcpServers hangs, the command blocks indefinitely.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/components/extensions/tabs/InstalledTab.tsx Outdated
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts
Comment thread packages/cli/src/config/hot-reload.ts
@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch from 9ebd502 to 0b6fff1 Compare June 30, 2026 12:58
Comment thread packages/cli/src/ui/components/extensions/tabs/DiscoverTab.tsx Outdated
Comment thread packages/cli/src/ui/hooks/useExtensionUpdates.ts Outdated
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch from 0b6fff1 to 65134d6 Compare July 1, 2026 03:05
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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)为单个提交。

Comment thread packages/cli/src/ui/components/extensions/tabs/SourcesTab.tsx Outdated
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/core/src/extension/extensionManager.ts Outdated
Comment thread packages/cli/src/ui/components/extensions/tabs/SourcesTab.tsx Outdated
Comment thread packages/cli/src/ui/components/extensions/views/ExtensionActionsView.tsx Outdated
俊良 added 2 commits July 1, 2026 12:38
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.
@ZijianZhang989
ZijianZhang989 force-pushed the feat/settings-refresh-classification branch from 65134d6 to 48ece8d Compare July 1, 2026 04:42

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent Review Summary

Thorough review of 91 changed files (+5538/-824) across all subsystems touched by this PR:

Areas reviewed

  1. Plugin reload command (/reload-plugins, plugin-refresh-state.ts, hot-reload.ts): Stale flag lifecycle, clearPluginCaches error isolation, reloadPluginsRuntime call ordering, command registration. All previously flagged issues (||&& guard, try/catch isolation for clearPluginCaches, ExtensionRuntimeRefreshOptions export) have been addressed.

  2. Workspace memory remember (workspace-remember.ts, workspace-remember-errors.ts, remember.ts, bridge runWorkspaceMemoryRemember/isWorkspaceMemoryRememberAvailable): Task lane serialization via this.tail.then(run, run), content size validation (64KB limit), error code extraction chain, classifyTouchedScopes path escape detection, chat recording suppression via AsyncLocalStorage. Implementation is sound.

  3. Channel daemon worker (channel-worker-supervisor.ts, daemon-worker.ts, channel-worker-env.ts): Process lifecycle (spawn → ready message → SIGTERM → SIGKILL escalation), startup timeout with unref(), 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.

  4. Session loop detection (Session.ts): recordDaemonInvalidToolParams bucketing by tool name only, fillLoopSkippedFrom/fillPermissionSkippedFrom for batch cancellation, sequential-then-parallel execution when loop state is active, recordSkippedToolCall callback propagation. The LOOP_DETECTED_CONTEXT_MESSAGE is correctly appended to preserved tool runs.

  5. Bridge workspace control (bridge.ts): hasNoChannelWork with spawn/restore/workspace-control in-flight counting, reapPendingEmptyChannel for deferred teardown, withWorkspaceControl reference counting. Concurrency guards correctly prevent negative counts via Math.max(0, ...).

  6. Mouse interactions (useMouseEvents.ts, RowMouseController, TextInputMouseController): Reference-counted mouse mode tracking with level promotion (buttonany), frame anchor calculation for bottom-anchored layouts, visual-to-logical click mapping with wide character support.

  7. SDK types (types.ts, events.ts, acpRouteTable.ts, AcpWsTransport.ts): New DaemonWorkspaceMemoryRemember* types, DaemonManagedMemoryChangedData discriminated union, route table entries for /workspace/memory/remember, httpStatus extraction 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: falsetrue) 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.

Comment thread packages/cli/src/ui/components/extensions/tabs/DiscoverTab.tsx
Comment thread packages/cli/src/config/hot-reload.ts
Comment thread packages/cli/src/ui/commands/reload-plugins-command.ts
Comment thread packages/core/src/extension/extensionManager.ts

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 },
});
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

@wenshao

wenshao commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

🔬 Local verification report (maintainer)

Built the real binary at PR head 48ece8de and verified behavior via unit tests, mutation testing, and a real‑TUI end‑to‑end run.

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

  • Worktree at PR head 48ece8de — the 2 PR commits sit cleanly on top of origin/main (merge‑base = current origin/main HEAD).
  • npm ci → build core → build cli (tsc) → real binary node packages/cli/dist/index.js (v0.19.3).

✅ Unit tests — all green

  • packages/core extensionManager.test.ts: 57 passed (incl. the new refreshTools option cases).
  • packages/cli — all 7 PR‑touched suites pass: plugin-refresh-state, hot-reload (24), reload-plugins-command, BuiltinCommandLoader, extensionsCommand (26), ExtensionsManagerDialog (13), useExtensionUpdates (16).

🧪 Mutation testing — core invariants are load‑bearing

Introduced 4 targeted mutations; each made the intended test fail (then reverted), proving the tests actually hold the behavior:

# Mutation Test that caught it Failure
1 Remove idempotency guard in markPluginsChanged deduplicates plugin refresh notifications expected true to be false
2 Clear the stale flag in the catch (failure path) surfaces reload failures without clearing the refresh flag clearPluginsChanged called 1× (expected 0×)
3 enableExtension ?? true?? false defaults to refreshing tools when options are omitted refreshTools called 0× (expected 1×)
4 Drop refreshTools() inside reloadPluginsRuntime both reloadPluginsRuntime cases refreshTools not called

🖥️ Real TUI E2E (tmux + real binary, isolated QWEN_HOME, 2 test extensions)

  1. /reload-plugins is registered and runs → Reloaded: 2 plugins · 0 commands · 0 skills · 0 hooks · 0 plugin MCP servers · 0 plugin LSP servers — the count reflects the 2 active extensions (not hardcoded).
  2. Toggle an extension in /extensions manage (Installed tab) → chat shows Plugin changes detected. Run /reload-plugins to apply them.
  3. Dedup: a 2nd toggle without reloading produced no duplicate notification (1 total).
  4. Clear lifecycle: after /reload-plugins cleared the flag, the next toggle re‑notified (2 total) — the full markPluginsChanged / clearPluginsChanged lifecycle is confirmed in the real app.
  5. clearPluginCaches runs inline on each toggle without crashing (toggle succeeds + notification fires).

(The "failed reload keeps the flag set" path was not force‑triggered in the TUI, but it is covered by a load‑bearing unit test — mutation #2 above.)

⚠️ Worth weighing before merge (tradeoffs, not defects)

  1. refreshTools: false on disable/uninstall. At head 48ece8de, every disable/uninstall UI call passes { refreshTools: false } (InstalledTab.tsx:467, ExtensionActionsView.tsx:119 / 257 / 323). Effect: on disable/uninstall, clearPluginCaches refreshes skills + subagents immediately, but MCP‑server teardown (restartMcpServers), hierarchical memory (context), and tool registration are deferred to /reload-plugins. This is the bot's original [Critical] (a removed extension's MCP server keeps running until reload). The PR frames it as an acknowledged pre‑existing gap (no pruneRemovedPluginHooks equivalent) with a follow‑up — a reasonable call, but a real behavior change for anyone relying on immediate teardown.
    • 🔺 Discrepancy to confirm: the RESOLVED review reply states "I've updated disable/uninstall to use the default refreshTools: true so the extension's MCP servers, skills, agents, and context are torn down immediately." The current code still uses refreshTools: false, so MCP servers + context are NOT torn down immediately (only skills + subagents are). Please confirm refreshTools: false is the intended final state.
  2. /extensions install no longer calls reloadCommands(). Slash commands from a freshly installed extension now appear only after /reload-plugins. Documented in the PR; UX note only.

📝 Open non‑blocking [Suggestion]s (latest bot review — all valid)

  • Concurrent auto‑updates each call clearPluginCaches → N redundant FS walks (debounce candidate).
  • ExtensionsManagerDialog.test.tsx covers the disable branch only, not enable.
  • updateExtension still takes a positional boolean while siblings use { refreshTools } — not self‑documenting.

🔎 Minor

  • clearPluginCaches guard uses &&: if (!getSkillManager && !getSubagentManager) return. If a Config ever had exactly one of the two getters, the Promise.allSettled([...]) array would throw a TypeError (optional chaining guards ?.refreshCache, not the getXxxManager() call). Not reachable with a real Config (always has both) and callers wrap it in try/catch — noted for completeness.
  • clearPluginCaches has no dedicated unit test (as the bot noted); the E2E exercised it indirectly.

CI

  • Test (ubuntu-latest)pass (the leg that runs lint / typecheck / i18n static checks).
  • reviewDecision: CHANGES_REQUESTED is stale from an earlier bot review; the latest bot review says "no blocking issues" and qqqys (GPT‑5) approved at 48ece8de.

Bottom line: functionality is correct and well‑tested. Recommend confirming the disable/uninstall refreshTools: false intent (vs. the RESOLVED reply) before merge; the rest are non‑blocking.

🀄 中文版(完整对应)

🔬 本地验证报告(维护者)

PR head 48ece8de 构建了真实二进制,通过单元测试、变异测试、真实 TUI 端到端运行验证行为。

结论: PR 声称的行为正确且覆盖充分。合并前有 1 处「代码 vs 评审回复」的矛盾 和 2 处有意的设计权衡值得看一下;其余均为非阻塞项。

环境

  • worktree 停在 PR head 48ece8de——PR 的 2 个提交干净地落在 origin/main 之上(merge‑base = 当前 origin/main HEAD)。
  • npm ci → 构建 core → 构建 cli(tsc)→ 真实二进制 node packages/cli/dist/index.js(v0.19.3)。

✅ 单元测试——全绿

  • packages/core extensionManager.test.ts57 passed(含新增的 refreshTools 选项用例)。
  • packages/cli——7 个 PR 涉及的测试文件全部通过:plugin-refresh-statehot-reload(24)、reload-plugins-commandBuiltinCommandLoaderextensionsCommand(26)、ExtensionsManagerDialog(13)、useExtensionUpdates(16)。

🧪 变异测试——核心不变式的测试真正承重

引入 4 个定向变异,每个都让对应测试翻红(随后还原),证明测试不是空过:

# 变异 抓到它的测试 失败信息
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 个测试扩展)

  1. /reload-plugins 已注册并可运行 → Reloaded: 2 plugins · 0 commands · 0 skills · 0 hooks · 0 plugin MCP servers · 0 plugin LSP servers——计数反映 2 个 active 扩展(非硬编码)。
  2. /extensions manage(Installed 标签页)toggle 一个扩展 → 聊天出现 Plugin changes detected. Run /reload-plugins to apply them.
  3. 去重: 不重载再 toggle 第二次,没有重复通知(总计 1 条)。
  4. 清除生命周期: /reload-plugins 清 flag 后,下一次 toggle 重新通知(总计 2 条)——完整的 markPluginsChanged / clearPluginsChanged 生命周期在真实 app 里得到确认。
  5. 每次 toggle 内联执行 clearPluginCaches 无崩溃(toggle 成功 + 通知触发)。

(「失败重载保留 flag」的路径未在 TUI 里强制触发,但已由承重的单元测试覆盖——即上面的变异 #2。)

⚠️ 合并前值得权衡(是权衡,不是缺陷)

  1. disable/uninstall 用 refreshTools: false 在 head 48ece8de,所有 disable/uninstall 的 UI 调用都传 { refreshTools: false }InstalledTab.tsx:467ExtensionActionsView.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 是否为最终意图。
  2. /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 回复);其余均非阻塞。

@ZijianZhang989
ZijianZhang989 marked this pull request as draft July 1, 2026 11:01
@ZijianZhang989

Copy link
Copy Markdown
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category/cli Command line interface and interaction scope/extensions Extension configuration status/in-review This issue is currently in review. type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants