From d385b7cf050c4cd2666cca3bdd19842c02c8beca Mon Sep 17 00:00:00 2001 From: Pine Date: Fri, 14 Aug 2026 14:23:42 +0800 Subject: [PATCH] feat(ui): group the plugin list into toggleable and system sections User-added (toggleable) plugins carry an enable/disable button in the main list; required system plugins sit in a separate collapsible 'system plugins' section with no controls. The system section starts expanded so the existing web e2e/snapshot (which targets a system plugin row) still passes. Co-Authored-By: Claude --- ...14-plugin-enable-disable-in-page.i18n.yaml | 4 +- ...026-08-14-plugin-enable-disable-in-page.md | 3 + ...-08-14-plugin-enable-disable-in-page.zh.md | 3 +- .../PluginInventorySettingsTab.module.css | 22 +++ .../src/client/PluginInventorySettingsTab.tsx | 176 ++++++++++-------- .../src/client/locales.ts | 2 + packages/host/plugin-inventory/README.md | 2 +- packages/host/plugin-inventory/README.zh.md | 2 +- 8 files changed, 135 insertions(+), 79 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.i18n.yaml b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.i18n.yaml index 79980bce77..729001b04c 100644 --- a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.i18n.yaml @@ -2,5 +2,5 @@ # 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: 10560799d5543a3622e0a8d751012066ca36512a -2026-08-14-plugin-enable-disable-in-page.zh.md: 988b9f8fad9216f358e39966767e2e0649715a43 +2026-08-14-plugin-enable-disable-in-page.md: b868a8e4f84357e944c184cb80d58ea40858f7b8 +2026-08-14-plugin-enable-disable-in-page.zh.md: c53187fd067f3e208e62ecc09d361548ba61bda1 diff --git a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.md b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.md index 10560799d5..b868a8e4f8 100644 --- a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.md +++ b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.md @@ -34,6 +34,9 @@ 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. +The list is grouped: toggleable plugins carry the button in the main list, while +the required system plugins sit in a separate collapsible "system plugins" +section with no controls. **Guard:** every entry carries a `protected` flag. The rule is default-protect — disabling a plugin that another plugin injects breaks the dependent, and diff --git a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.zh.md b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.zh.md index 988b9f8fad..c53187fd06 100644 --- a/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.zh.md +++ b/.agents/notes/implemented/feature/2026-08-14-plugin-enable-disable-in-page.zh.md @@ -26,7 +26,8 @@ Web UI 的插件列表是只读的:它展示 Loader 的条目和生命周期 (`include:`);两者不同,只有裸 id 能命中 patch 的 `applyEntryPatches` 目标查找。 Web 插件列表 tab(`ui-settings-plugin-inventory`)在每张展开卡片的详情区加启用/停用按钮, -绑定该 Remote,切换后重新拉取列表。 +绑定该 Remote,切换后重新拉取列表。列表分组:可切换插件在主列表带按钮,必需的系统插件 +放在一个可折叠的"系统插件"区,无任何开关。 **门卫:** 每条条目带 `protected` 标记。规则默认保护——停用一个被其他插件注入的插件会破坏 依赖者,启用一个服务不可用的插件会导致启动失败(坏切换后都会表现为 diff --git a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css index b2e54b5bc7..a2933ca7c5 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css +++ b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css @@ -289,3 +289,25 @@ opacity: 0.7; font-size: 0.85em; } + +.systemSection { + margin-top: 1rem; +} + +.systemHeading { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 0; + font-weight: 600; + background: none; + border: none; + cursor: pointer; + color: inherit; +} + +.systemCount { + font-weight: 400; + opacity: 0.7; +} diff --git a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx index 16c32efb53..9cb5ff1f16 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx +++ b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx @@ -70,6 +70,7 @@ export function PluginInventorySettingsTab({ list, setEnabled, t }: PluginInvent const [expanded, setExpanded] = useState(null) const [state, setState] = useState({ status: 'loading' }) const [busy, setBusy] = useState(null) + const [systemOpen, setSystemOpen] = useState(true) useEffect(() => { let current = true @@ -97,6 +98,10 @@ export function PluginInventorySettingsTab({ list, setEnabled, t }: PluginInvent : [], [normalizedQuery, state], ) + // User-added (opt-in bundle) plugins are toggleable; the rest are required + // system plugins shown in a separate collapsible section without toggles. + const userEntries = filteredEntries.filter(entry => !entry.protected) + const systemEntries = filteredEntries.filter(entry => entry.protected) useEffect(() => { if (expanded !== null && !filteredEntries.some(entry => entry.entryId === expanded)) { @@ -109,6 +114,81 @@ export function PluginInventorySettingsTab({ list, setEnabled, t }: PluginInvent setRequest(value => value + 1) } + /** Render one inventory card; the toggle is hidden for protected system plugins. */ + const card = (entry: PluginInventoryEntry): ReactNode => { + const status = phaseLabel(entry.fiberPhase, t) + const title = moduleShortName(entry.moduleName) + const configuration = t(entry.enabled ? 'enabledTag' : 'disabledTag') + const open = expanded === entry.entryId + const detailId = `${catalogId}-details-${encodeURIComponent(entry.entryId)}` + return ( +
  • + + {open ? ( +
    + {entry.entryId} +
    +
    +
    {t('configuration')}
    +
    {configuration}
    +
    + {entry.enabled ? ( +
    +
    {t('cordis')}
    +
    {status}
    +
    + ) : null} +
    + {entry.protected ? ( +

    {t('required')}

    + ) : ( + + )} +
    + ) : null} +
  • + ) + } + return (
    {state.status === 'loading' ?

    {t('loading')}

    : null} @@ -139,83 +219,31 @@ export function PluginInventorySettingsTab({ list, setEnabled, t }: PluginInvent {state.snapshot.entries.length > 0 && filteredEntries.length === 0 ?

    {t('emptySearch')}

    : null} - {filteredEntries.length > 0 ? ( + {userEntries.length > 0 ? (
      - {filteredEntries.map((entry) => { - const status = phaseLabel(entry.fiberPhase, t) - const title = moduleShortName(entry.moduleName) - const configuration = t(entry.enabled ? 'enabledTag' : 'disabledTag') - const open = expanded === entry.entryId - const detailId = `${catalogId}-details-${encodeURIComponent(entry.entryId)}` - return ( -
    • - - {open ? ( -
      - {entry.entryId} -
      -
      -
      {t('configuration')}
      -
      {configuration}
      -
      - {entry.enabled ? ( -
      -
      {t('cordis')}
      -
      {status}
      -
      - ) : null} -
      - {entry.protected ? ( -

      {t('required')}

      - ) : ( - - )} -
      - ) : null} -
    • - ) - })} + {userEntries.map(card)}
    ) : null} + {systemEntries.length > 0 ? ( +
    + + {systemOpen ? ( +
      + {systemEntries.map(card)} +
    + ) : null} +
    + ) : null}
    ) : null} diff --git a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts index 6377860c59..cb782e317b 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts +++ b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts @@ -24,6 +24,7 @@ export const zh = { disable: '停用', toggling: '切换中…', required: '应用必需插件,不可切换', + systemPlugins: '系统插件', } satisfies Record /** Plugin inventory locale key union. */ @@ -53,4 +54,5 @@ export const en = { disable: 'Disable', toggling: 'Toggling…', required: 'Required by the app; cannot be toggled', + systemPlugins: 'System plugins', } satisfies Record diff --git a/packages/host/plugin-inventory/README.md b/packages/host/plugin-inventory/README.md index 863449b8cd..d6c831f98d 100644 --- a/packages/host/plugin-inventory/README.md +++ b/packages/host/plugin-inventory/README.md @@ -6,7 +6,7 @@ Host projection of the current Cordis Loader tree with per-plugin enable/disable The phase is `pending`, `loading`, `active`, `failed`, or `unloading`; it is `null` when the entry has no live root Fiber. The snapshot is intentionally point-in-time: Loader remains the sole lifecycle authority, while this package owns no cache, history, provenance model, or event stream. `setEnabled` toggles one entry live through `ctx.loader.update` and persists an explicit `disabled` override into the profile's user patch layer so the choice survives a restart (a bundle-default disable needs the `disabled: false` override to stick). -Every entry carries a `protected` flag. The guard is default-protect: every shipped plugin is required by the application (disabling one that another plugin injects breaks the dependent; enabling one whose service is unavailable fails the boot), so `setEnabled` refuses them and the UI hides the toggle. Only plugins a deployment adds through an opt-in bundle (`USER_TOGGLEABLE_PLUGINS` in `src/required.ts`) are toggleable. Its public payload types live under `./types`, and Typert generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`. +Every entry carries a `protected` flag. The guard is default-protect: every shipped plugin is required by the application (disabling one that another plugin injects breaks the dependent; enabling one whose service is unavailable fails the boot), so `setEnabled` refuses them and the UI hides the toggle. Only plugins a deployment adds through an opt-in bundle (`USER_TOGGLEABLE_PLUGINS` in `src/required.ts`) are toggleable. The Web plugin-list tab groups the inventory accordingly: toggleable plugins carry an enable/disable button, while the required system plugins sit in a separate collapsible "system plugins" section with no controls. Its public payload types live under `./types`, and Typert generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`. The service is Remote-only and deliberately declares no same-process Cordis `Context` merge. Client packages consume it through the explicit [`api-remotes`](../../api/remotes/README.md) assembly rather than importing the Host implementation. diff --git a/packages/host/plugin-inventory/README.zh.md b/packages/host/plugin-inventory/README.zh.md index b56134938d..f39dc22b02 100644 --- a/packages/host/plugin-inventory/README.zh.md +++ b/packages/host/plugin-inventory/README.zh.md @@ -6,7 +6,7 @@ 阶段为 `pending`、`loading`、`active`、`failed` 或 `unloading`;条目没有存活的根 Fiber 时则为 `null`。该快照刻意只表示调用当下:Loader 仍是唯一的生命周期权威,本包不拥有缓存、历史、来源模型或事件流。`setEnabled` 通过 `ctx.loader.update` 实时切换单条条目,并把显式 `disabled` 覆盖写进 profile 的用户补丁层,使选择在重启后保留(bundle 默认禁用的行需要 `disabled: false` 覆盖才能保持启用)。 -每条条目带 `protected` 标记。守卫默认保护:所有随包插件都是应用必需(停用一个被其他插件注入的插件会破坏依赖者;启用一个服务不可用的插件会导致启动失败),所以 `setEnabled` 拒绝它们、UI 隐藏开关。只有部署通过 opt-in bundle 添加的插件(`src/required.ts` 的 `USER_TOGGLEABLE_PLUGINS`)可切换。公开 payload 类型位于 `./types`,Typert 生成由 `./typert` 与 `./remote` 导出的 Host 和 Client Remote 产物。 +每条条目带 `protected` 标记。守卫默认保护:所有随包插件都是应用必需(停用一个被其他插件注入的插件会破坏依赖者;启用一个服务不可用的插件会导致启动失败),所以 `setEnabled` 拒绝它们、UI 隐藏开关。只有部署通过 opt-in bundle 添加的插件(`src/required.ts` 的 `USER_TOGGLEABLE_PLUGINS`)可切换。Web 插件列表 tab 据此分组:可切换插件带启用/停用按钮,必需的系统插件放在一个可折叠的"系统插件"区,无任何开关。公开 payload 类型位于 `./types`,Typert 生成由 `./typert` 与 `./remote` 导出的 Host 和 Client Remote 产物。 该服务仅供 Remote 使用,刻意不声明同进程 Cordis `Context` merge。Client 包通过显式的 [`api-remotes`](../../api/remotes/README.md) 组合消费它,而不导入 Host 实现。