docs(plugin-inventory): document setEnabled, persistence, and the in-page toggle decision
Update the plugin-inventory READMEs from read-only to toggleable and record the in-page enable/disable capability as an Agent Note. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.md
|
||||
2026-08-14-plugin-enable-disable-in-page.md: 18e3ce19da7e23f10a9fd9a79bbbcc907378065a
|
||||
2026-08-14-plugin-enable-disable-in-page.zh.md: 4726f39a04d8157759f2d67553f04d749c601ba7
|
||||
@@ -0,0 +1,76 @@
|
||||
# Agent Note: In-page plugin enable/disable
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-14-plugin-enable-disable-in-page.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web UI's plugin list was read-only: it showed the Loader's entries and their
|
||||
lifecycle state but could not toggle a plugin on or off. Users wanted to
|
||||
enable/disable plugins from the page, with the choice surviving a restart.
|
||||
|
||||
## Decision
|
||||
|
||||
Extend `PluginInventoryGateway` (`packages/host/plugin-inventory`) from a
|
||||
read-only projection to one that also enables/disables. It now publishes a
|
||||
second direct Remote, `pluginInventory/setEnabled(entryId, enabled)`, which:
|
||||
|
||||
1. Calls `ctx.loader.update(entryId, { disabled: !enabled })` — `Entry.update`
|
||||
disposes or re-starts the plugin's fiber live (the same runtime path HMR's
|
||||
config refresh uses).
|
||||
2. Persists an explicit `disabled` override into the profile's user patch layer
|
||||
(`cordis.patch.yml`) so the choice survives a restart.
|
||||
|
||||
The persistence is written via `persistPluginDisabled` (`src/persist.ts`): it
|
||||
upserts `- id: <rowId> disabled: true|false` atomically. The state is **always
|
||||
written explicitly** — re-enabling writes `disabled: false`, because dropping
|
||||
the row would fall back to the bundle's own `disabled` default rather than the
|
||||
user's choice.
|
||||
|
||||
The patch row id is the entry's bare `options.id`, not the group-prefixed Loader
|
||||
tree id (`include:<rowId>`); the two differ and only the bare id matches the
|
||||
patch's `applyEntryPatches` target lookup.
|
||||
|
||||
The Web plugin-list tab (`ui-settings-plugin-inventory`) adds an enable/disable
|
||||
button to each expanded card, wired to the Remote, re-listing after the toggle.
|
||||
|
||||
## Persistence caveat
|
||||
|
||||
A runtime toggle alone does not survive a restart for a row enabled by a bundle
|
||||
patch, because `Entry.update` writes the fully-patched tree and the patch layer
|
||||
re-applies on the next read. Writing the override into the profile's
|
||||
`cordis.patch.yml` (the last-applied user layer) is what makes it durable. The
|
||||
web profile has HMR off, so the immediate live effect comes from
|
||||
`loader.update`, not from the file write; the file matters only on restart.
|
||||
|
||||
## Verification
|
||||
|
||||
- `persistPluginDisabled` unit tests: append, override-a-bundle-disable, and
|
||||
dedupe an existing override.
|
||||
- `PluginInventoryGateway.setEnabled` test: toggles the Loader entry live.
|
||||
- `remoteMethods` includes `setEnabled`.
|
||||
- Real use: toggle a plugin in the plugin-list tab, confirm its fiber phase
|
||||
changes and the profile `cordis.patch.yml` carries the override; restart and
|
||||
confirm the choice holds.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Runtime-only toggle (no persistence).** Rejected: the user asked for the
|
||||
choice to survive a restart, which a bare `loader.update` cannot guarantee for
|
||||
a bundle-patch-disabled row.
|
||||
- **Persist by writing the fully-patched tree to the base config.** Rejected:
|
||||
the profile root config is an empty entry list; dumping the whole composed
|
||||
tree there would corrupt it. The durable write must target the user patch
|
||||
layer instead.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Costs:** the gateway grows from read-only to writable, adding a Remote and a
|
||||
profile-patch write path; toggling a row the profile does not mount (absent
|
||||
from every bundle) is unsupported; re-enabling always writes `disabled: false`
|
||||
so the patch carries a row even for a default-enabled plugin the user turned
|
||||
back on.
|
||||
- **Buys:** users can enable/disable plugins from the page with the choice
|
||||
surviving a restart, using the Loader's existing runtime update path plus an
|
||||
explicit override in the last-applied user layer.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Agent Note:页面内开关插件
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-14-plugin-enable-disable-in-page.md)
|
||||
|
||||
## 问题
|
||||
|
||||
Web UI 的插件列表是只读的:它展示 Loader 的条目和生命周期状态,但不能在页面内开关插件。
|
||||
用户希望能在页面内启用/停用插件,且重启后保留选择。
|
||||
|
||||
## 决策
|
||||
|
||||
把 `PluginInventoryGateway`(`packages/host/plugin-inventory`)从只读投影扩展为可启停。
|
||||
新增第二个直接 Remote `pluginInventory/setEnabled(entryId, enabled)`,它:
|
||||
|
||||
1. 调用 `ctx.loader.update(entryId, { disabled: !enabled })` —— `Entry.update`
|
||||
实时 dispose/重启插件 fiber(与 HMR 配置刷新用的同一条运行时路径)。
|
||||
2. 把显式 `disabled` 覆盖写进 profile 的用户补丁层(`cordis.patch.yml`),使选择在重启后保留。
|
||||
|
||||
持久化经 `persistPluginDisabled`(`src/persist.ts`)实现:原子 upsert
|
||||
`- id: <rowId> disabled: true|false`。状态**始终显式写入**——重新启用写
|
||||
`disabled: false`,因为删掉该行会回落到 bundle 自身的 `disabled` 默认,而不是用户的选择。
|
||||
|
||||
补丁行的 id 用条目的裸 `options.id`,而非带组前缀的 Loader 树 id
|
||||
(`include:<rowId>`);两者不同,只有裸 id 能命中 patch 的 `applyEntryPatches` 目标查找。
|
||||
|
||||
Web 插件列表 tab(`ui-settings-plugin-inventory`)在每张展开卡片的详情区加启用/停用按钮,
|
||||
绑定该 Remote,切换后重新拉取列表。
|
||||
|
||||
## 持久化注意
|
||||
|
||||
仅运行时 toggle 对由 bundle patch 启用的行不持久,因为 `Entry.update` 写回的是补丁后的整棵树,
|
||||
而 patch 层在下一次读取时会重放。把覆盖写进 profile 的 `cordis.patch.yml`(最后应用的用户层)
|
||||
才是持久的。web profile 的 HMR 关闭,所以立即生效来自 `loader.update`,而非文件写入;
|
||||
文件只在重启时起作用。
|
||||
|
||||
## 验证
|
||||
|
||||
- `persistPluginDisabled` 单测:追加、覆盖 bundle 默认禁用、去重已有覆盖。
|
||||
- `PluginInventoryGateway.setEnabled` 测试:实时切换 Loader 条目。
|
||||
- `remoteMethods` 包含 `setEnabled`。
|
||||
- 实机:在插件列表 tab 切换某插件,确认其 fiber phase 变化且 profile
|
||||
`cordis.patch.yml` 带上覆盖;重启后确认选择仍保留。
|
||||
|
||||
## 备选方案
|
||||
|
||||
- **仅运行时 toggle(不持久)。** 已否决:用户要求选择在重启后保留,而裸的
|
||||
`loader.update` 对 bundle patch 禁用的行无法保证这一点。
|
||||
- **把补丁后的整棵树写回 base config 以持久化。** 已否决:profile 根配置是空条目列表,
|
||||
把整棵组合树倒进去会破坏它。持久写必须落到用户补丁层。
|
||||
|
||||
## 后果
|
||||
|
||||
- **代价:** 网关从只读变为可写,新增一个 Remote 和一条写 profile patch 的路径;
|
||||
对 profile 未挂载(任何 bundle 都没有)的行不支持切换;重新启用总是写
|
||||
`disabled: false`,所以即使默认启用的插件被用户重新打开,patch 也会带一行。
|
||||
- **收益:** 用户可在页面内开关插件,且选择在重启后保留——复用 Loader 现有的运行时
|
||||
update 路径,并在最后应用的用户层写显式覆盖。
|
||||
Reference in New Issue
Block a user