From c2a5540ddb5d67cb05b326ecb54ada5edf1310ab Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 8 Sep 2026 21:05:37 +0800 Subject: [PATCH 01/93] fix(web): distinguish plugin configuration instances --- .../plugin-instances.expected.md | 37 +++++++++++++ .../settings-chrome/plugins.expected.md | 6 +-- apps/web/tests/settings-chrome.e2e.ts | 42 +++++++++++++-- .../README.i18n.yaml | 4 +- .../ui-settings-plugin-inventory/README.md | 6 +-- .../ui-settings-plugin-inventory/README.zh.md | 6 +-- .../PluginInventorySettingsTab.module.css | 24 +++++++-- .../src/client/PluginInventorySettingsTab.tsx | 9 ++-- .../tests/components.client.spec.tsx | 54 ++++++++++++++----- 9 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 apps/web/tests/expected/settings-chrome/plugin-instances.expected.md diff --git a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md new file mode 100644 index 0000000000..5ec98e1f86 --- /dev/null +++ b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md @@ -0,0 +1,37 @@ +- list: + - listitem: + - button "tool-subagent-control, tool-subagent-control, 已启用": + - strong: tool-subagent-control + - code: tool-subagent-control + - text: 已启用 + - img + - listitem: + - button "tool-subagent-control/list-agents, tool-subagent-list-agents, 已启用": + - strong: tool-subagent-control/list-agents + - code: tool-subagent-list-agents + - text: 已启用 + - img + - listitem: + - button "tool-subagent, tool-subagent, 已启用": + - strong: tool-subagent + - code: tool-subagent + - text: 已启用 + - img + - listitem: + - button "tool-subagent, tool-subagent-fork, 已启用": + - strong: tool-subagent + - code: tool-subagent-fork + - text: 已启用 + - img + - listitem: + - button "tool-subagent, tool-subagent-codex, 已停用": + - strong: tool-subagent + - code: tool-subagent-codex + - text: 已停用 + - img + - listitem: + - button "tool-subagent, tool-subagent-claude-code, 已停用": + - strong: tool-subagent + - code: tool-subagent-claude-code + - text: 已停用 + - img diff --git a/apps/web/tests/expected/settings-chrome/plugins.expected.md b/apps/web/tests/expected/settings-chrome/plugins.expected.md index c4e0cbf9f0..71e84da312 100644 --- a/apps/web/tests/expected/settings-chrome/plugins.expected.md +++ b/apps/web/tests/expected/settings-chrome/plugins.expected.md @@ -1,6 +1,6 @@ - listitem: - - button "ui-settings, 已启用": - - strong: ui-settings - - img "运行中" + - button "tool-subagent, tool-subagent, 已启用": + - strong: tool-subagent + - code: tool-subagent - text: 已启用 - img diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index f3be2fcd39..ea2d4d9276 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -24,9 +24,10 @@ import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/settings-chrome', import.meta.url)) const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md') const PLUGINS_EXPECTED = join(SNAPSHOT_DIR, 'plugins.expected.md') +const PLUGIN_INSTANCES_EXPECTED = join(SNAPSHOT_DIR, 'plugin-instances.expected.md') // The English fallback surface: a browser naming no shipped language. const DIALOG_EN_EXPECTED = join(SNAPSHOT_DIR, 'dialog-en.expected.md') -const PLUGIN_ROW_SELECTOR = '[data-plugin-entry$="ui-settings"]' +const PLUGIN_ROW_SELECTOR = '[data-plugin-scope="preset"] [data-plugin-entry="tool-subagent"]' const MODE = webSnapshotMode() describe('web e2e: settings modal and General preferences', () => { @@ -114,7 +115,8 @@ describe('web e2e: settings modal and General preferences', () => { const expectedPluginCount = [...scaffold.ctx.loader.entries()] .filter(entry => !entry.options.group) .length - expect(await dialog.getByRole('searchbox', { name: '搜索插件' }).count()).toBe(1) + const pluginSearch = dialog.getByRole('searchbox', { name: '搜索插件' }) + expect(await pluginSearch.count()).toBe(1) // Every Loader entry appears exactly once in the global group — rows the // presets took over included, preset compositions excluded. expect(await dialog.locator('[data-plugin-scope="global"] [data-plugin-entry]').count()) @@ -130,6 +132,35 @@ describe('web e2e: settings modal and General preferences', () => { scaffold.workspaceCwd, ) await compareOrRefreshGolden(PLUGINS_EXPECTED, pluginsSnapshot, MODE) + await pluginSearch.fill('tool-subagent') + const instanceRows = [ + ['tool-subagent', '已启用'], + ['tool-subagent-fork', '已启用'], + ['tool-subagent-codex', '已停用'], + ['tool-subagent-claude-code', '已停用'], + ] as const + for (const [entryId, status] of instanceRows) { + const row = dialog.locator(`[data-plugin-scope="preset"] [data-plugin-entry="${entryId}"]`) + const trigger = row.getByRole('button', { name: `tool-subagent, ${entryId}, ${status}`, exact: true }) + await trigger.waitFor({ timeout: 10_000 }) + expect(await trigger.getAttribute('aria-expanded')).toBe('false') + const identity = row.locator('code') + expect(await identity.textContent()).toBe(entryId) + expect(await identity.getAttribute('title')).toBe(entryId) + } + const instancesSnapshot = await captureStableAria( + page, + '[data-plugin-scope="preset"] ul', + scaffold.workspaceCwd, + ) + await compareOrRefreshGolden(PLUGIN_INSTANCES_EXPECTED, instancesSnapshot, MODE) + await dialog.getByRole('button', { + name: 'tool-subagent, tool-subagent-claude-code, 已停用', + exact: true, + }).click() + expect(await dialog.locator('[data-plugin-entry="tool-subagent-claude-code"] button') + .getAttribute('aria-expanded')).toBe('true') + await pluginSearch.fill('') // Close path 1: Escape. await page.keyboard.press('Escape') await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0) @@ -635,6 +666,11 @@ describe('web e2e: settings modal and General preferences', () => { it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { expect(tripwire.warnings).toEqual([]) - await assertFixtureInventory(SNAPSHOT_DIR, ['dialog-en.expected.md', 'dialog.expected.md', 'plugins.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, [ + 'dialog-en.expected.md', + 'dialog.expected.md', + 'plugin-instances.expected.md', + 'plugins.expected.md', + ]) }) }) diff --git a/packages/client/ui-settings-plugin-inventory/README.i18n.yaml b/packages/client/ui-settings-plugin-inventory/README.i18n.yaml index 3be2ee8298..5d5a900ece 100644 --- a/packages/client/ui-settings-plugin-inventory/README.i18n.yaml +++ b/packages/client/ui-settings-plugin-inventory/README.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 packages/client/ui-settings-plugin-inventory/README.md -README.md: 9a7ea91521c08dd1855925af59521d3af6e47b25 -README.zh.md: 2427d667d92b7a4c620fc6222507ecd8c2fabcef +README.md: 2a7a00f037905c9e70edb283c22946a2bc6c0247 +README.zh.md: 7be131a8a1a3f1e1fbbf2fbff1ac65c1829f4c10 diff --git a/packages/client/ui-settings-plugin-inventory/README.md b/packages/client/ui-settings-plugin-inventory/README.md index 9a7ea91521..2a7a00f037 100644 --- a/packages/client/ui-settings-plugin-inventory/README.md +++ b/packages/client/ui-settings-plugin-inventory/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It presents agent-preset compositions first and keeps the global inventory collapsed until needed. Cards expose enablement, provenance, runtime status, disabled conditions, and discovery failures; global entries supplied by presets are identified with the enabling presets. Search covers both groups and points to matches in other presets. The tab supports loading, empty, no-match, failure, and retry states without exposing transport details, and it still shows the global inventory when no preset roster is available. +The **Plugin list** tab lets Web users inspect plugins without changing their configuration. It presents agent presets first and collapses the global inventory until needed. Cards retain the package name as the primary title, identify instances by stable entry id, and expose enablement, provenance, runtime status, disabled conditions, and discovery failures; preset-provided global entries name their presets. Search covers both groups and points to matches in other presets. The tab handles loading, empty, no-match, failure, and retry states without exposing transport details, and still shows the global inventory without a preset roster. ## Table of Contents @@ -29,7 +29,7 @@ Open the Plugins section in Settings and select the **Plugin list** tab to inspe ### Reading a card -Each collapsed card uses the short module name as its title and a small enablement tag; enabled entries also show a colored root-fiber status dot. Expanding one card reveals the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-presets/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset files' Chinese names. Search filters both groups by module name and entry id. +Each collapsed card uses the short module name as its primary title, shows the stable entry id underneath, and carries a small enablement tag; enabled entries also show a colored root-fiber status dot. Long entry ids truncate in the row and remain available on hover. Expanding one card reveals the declared entry id, the full module specifier, and the state facts: a preset row names the preset it comes from, its runtime status when the composition is live, and its disable condition when it carries one; a preset-provided global row explains that agent presets provide it per session, names the presets that enable it, and offers a jump into the preset group. Preset names resolve through the shared `presetDisplayText` fold (`dsh-agent-presets/display`) over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries: shipped presets follow the active locale while user-authored ones keep their own metadata, so an English surface never echoes the preset files' Chinese names. Search filters both groups by module name and entry id. ### The preset switcher @@ -55,7 +55,7 @@ The browser plugin registers one localized `settings.plugins.tab` contribution w ### Rendering -Row keys are scope-qualified (`global:`, `preset::`), so one module appearing in both scopes keeps distinct disclosure state; an entry id is shown as detail only when the row declares one and is never classified by string shape. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision. +Row keys are scope-qualified (`global:`, `preset::`), so one module appearing in both scopes keeps distinct disclosure state; a declared entry id appears in the collapsed row and expanded detail, while a row without one stays unlabeled, and the renderer never classifies ids by string shape. The preset-provided marking is derived client-side: a global entry carries it when it is disabled there while at least one preset row for the same module specifier is actually enabled, so a module every preset gates off (or declares only conditionally) stays plainly disabled rather than over-claiming provision. diff --git a/packages/client/ui-settings-plugin-inventory/README.zh.md b/packages/client/ui-settings-plugin-inventory/README.zh.md index 2427d667d9..7be131a8a1 100644 --- a/packages/client/ui-settings-plugin-inventory/README.zh.md +++ b/packages/client/ui-settings-plugin-inventory/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它优先展示 Agent 预设组合,并在需要前收起全局清单。卡片展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会标明启用它的预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页支持加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。 +**插件列表**标签页让 Web 用户查看插件,而不改变其配置。它优先展示 Agent 预设,并在需要前收起全局清单。卡片保留包名作为主标题,以稳定的条目 id 标识实例,并展示启停状态、出处、运行状态、禁用条件与发现失败;由预设提供的全局条目会列出对应预设。搜索覆盖两个分组,并指出其他预设中的匹配。标签页处理加载、空结果、无匹配、失败与重试状态,且不暴露传输细节;没有预设 roster 时仍会展示全局清单。 ## 目录 @@ -29,7 +29,7 @@ kind: "package-reference" ### 阅读卡片 -每张收起的卡片使用模块短名称作为标题,并以小标签表示启停状态;已启用的条目还会显示彩色根 fiber 状态圆点。展开卡片后会显示声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-presets/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设文件里的中文名。搜索按模块名称与条目 id 过滤两组。 +每张收起的卡片使用模块短名称作为主标题,在下方显示稳定的条目 id,并以小标签表示启停状态;已启用的条目还会显示彩色根 fiber 状态圆点。长条目 id 会在行内截断,悬停时仍可查看完整值。展开卡片后会显示声明的条目 id、完整模块标识与状态事实:预设行说明它来自哪个预设、组合存活时的运行状态,以及它携带的禁用条件;被预设提供的全局行说明它由 Agent 预设按会话提供、列出启用它的预设,并提供跳转到预设组的入口。预设名经共享的 `presetDisplayText` 纯函数(`dsh-agent-presets/display`)叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析:内置预设走当前语言,用户自建预设保留自己的元数据,因此英文界面不会回显预设文件里的中文名。搜索按模块名称与条目 id 过滤两组。 ### 预设切换器 @@ -55,7 +55,7 @@ kind: "package-reference" ### 渲染 -行 key 按作用域限定(`global:`、`preset::`),因此同一模块出现在两个作用域时保持各自的展开状态;条目 id 只在行声明了它时作为详情展示,代码不按字符串形状对它分类。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。 +行 key 按作用域限定(`global:`、`preset::`),因此同一模块出现在两个作用域时保持各自的展开状态;声明的条目 id 同时出现在收起行与展开详情中,没有 id 的行不显示次级标签,渲染器也不按字符串形状对 id 分类。预设提供标记在客户端推导:一个全局条目在全局被停用、且至少一个预设行对同一模块标识实际启用时才携带它,因此被所有预设关掉(或仅条件声明)的模块保持单纯的已停用,而不是夸大提供关系。 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 d914492230..35ea6e7fbc 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 @@ -150,14 +150,32 @@ outline-offset: -2px; } -.cardTitle { +.cardHeading { + display: flex; + flex: 1; + flex-direction: column; min-width: 0; overflow: hidden; +} + +.cardTitle, +.cardIdentity { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cardTitle { font-size: 14px; line-height: 20px; font-weight: 600; - text-overflow: ellipsis; - white-space: nowrap; +} + +.cardIdentity { + color: var(--dsw-alias-label-tertiary); + font-family: var(--ds-font-family-code); + font-size: 11px; + line-height: 16px; } .cardTrailing { 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 31e37a6ca0..b062f1b157 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx +++ b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx @@ -113,7 +113,10 @@ function PluginCard({ rowKey, moduleName, entryId, trailing, ariaLabel, failed, aria-label={ariaLabel} onClick={() => { onToggle(rowKey) }} > - {moduleShortName(moduleName)} + + {moduleShortName(moduleName)} + {entryId === null ? null : {entryId}} + {trailing}