From 0f06f973c411c99bc391afdb7bdff20b057c441d Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sat, 29 Aug 2026 17:13:45 +0800 Subject: [PATCH] refactor(plugin-inventory): settings-row style group headers Both scope groups drop their boxed chrome for the General-settings row idiom: a title row (session-plugins title with the right-aligned preset selector pill; global-plugins title), a grey subtitle line carrying the count, a hairline divider between groups, and the cards grid below. Counts ride the subtitle as text while the data attributes keep the raw numbers for tests. --- .../PluginInventorySettingsTab.module.css | 54 +++++------ .../src/client/PluginInventorySettingsTab.tsx | 95 ++++++++++--------- .../src/client/locales.ts | 12 ++- .../tests/components.client.spec.tsx | 20 ++-- 4 files changed, 94 insertions(+), 87 deletions(-) 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 60673d222f..fa312a68fa 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 @@ -225,21 +225,29 @@ display: flex; flex-direction: column; gap: 10px; - border: 1px solid var(--dsw-alias-border-l2); - border-radius: 12px; - padding: 10px 12px 12px; } -.groupHeader, -.groupToggle { +.groupTitleRow { display: flex; align-items: center; gap: 8px; - min-height: 28px; + min-height: 32px; +} + +.group + .group { + border-top: 1px solid var(--dsw-alias-border-l2); + padding-top: 14px; +} + +.headerEnd { + margin-left: auto; } .groupToggle { - width: 100%; + display: flex; + flex: none; + align-items: center; + gap: 8px; border: 0; padding: 0; background: transparent; @@ -263,22 +271,17 @@ } .groupTitle { - font-size: 13px; - line-height: 20px; - font-weight: 600; + font-size: 14px; + line-height: 22px; + font-weight: 500; + color: var(--dsw-alias-label-primary); } -.groupSubtitle { - overflow: hidden; - color: var(--dsw-alias-label-tertiary); - font-size: 12px; - line-height: 18px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.groupCount { - margin-left: auto; +.groupSub { + display: flex; + flex-wrap: wrap; + gap: 4px 8px; + margin: -6px 0 0 20px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; @@ -286,10 +289,7 @@ } .failedCount { - flex: none; color: var(--dsw-alias-state-error-primary); - font-size: 12px; - line-height: 18px; } /* The preset switcher trigger mirrors the General-settings selector pill. */ @@ -331,12 +331,6 @@ text-overflow: ellipsis; } -.iconToggle { - flex: none; - width: auto; - min-height: 0; - padding: 4px; -} .groupBody { display: flex; 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 e56cbfdef8..fa78d36872 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx +++ b/packages/client/ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx @@ -365,46 +365,51 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT {selected !== undefined ? (
-
+
- { setSwitcherOpen(false) }} - items={presets.map(preset => ({ id: preset.id, label: presetLabel(preset, t) }))} - selectedId={selected.id} - onSelect={(id) => { - setSwitcherOpen(false) - setChosenPreset(id) - }} - portal - anchor={( - - )} - /> - {t('presetSubtitle')} - - {selectedRows.length} - +
+ { setSwitcherOpen(false) }} + items={presets.map(preset => ({ id: preset.id, label: presetLabel(preset, t) }))} + selectedId={selected.id} + onSelect={(id) => { + setSwitcherOpen(false) + setChosenPreset(id) + }} + align="end" + portal + anchor={( + + )} + /> +
+

+ {t('presetSubtitle')} + + {` · ${String(selectedRows.length)} ${t('countUnit')}`} + +

{presetEffectiveOpen ? (
{selected.broken !== undefined ? ( @@ -437,21 +442,25 @@ export function PluginInventorySettingsTab({ list, t }: PluginInventorySettingsT {entries.length > 0 ? (
- +
+

+ {t('globalSubtitle')} + {` · ${String(globalCount)} ${t('countUnit')}`} {filteredFailed.length > 0 ? ( {filteredFailed.length} {t('failedCountLabel')} ) : null} - +

{globalEffectiveOpen && globalCount > 0 ? (
    {filteredFailed.map(entry => globalRowCard(entry))} 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 8bad8a698e..603c1ce38b 100644 --- a/packages/client/ui-settings-plugin-inventory/src/client/locales.ts +++ b/packages/client/ui-settings-plugin-inventory/src/client/locales.ts @@ -9,11 +9,13 @@ export const zh = { search: '搜索插件', empty: '暂无插件。', emptySearch: '没有匹配的插件。', - presetSubtitle: '会话使用的插件', + presetTitle: '会话插件', + presetSubtitle: '由 Agent 预设按会话组成', + countUnit: '个', switcherLabel: '选择要查看的 Agent 预设', presetOptionDefault: '{name}(默认)', presetOptionBroken: '{name}(加载失败)', - globalTitle: '全局', + globalTitle: '全局插件', globalSubtitle: '系统与所有会话共用', presetProvidedDetail: '全局已停用,由 Agent 预设按会话提供', enabledIn: '启用于', @@ -50,11 +52,13 @@ export const en = { search: 'Search plugins', empty: 'No plugins are available.', emptySearch: 'No matching plugins.', - presetSubtitle: 'Plugins your sessions run', + presetTitle: 'Session plugins', + presetSubtitle: 'Composed per session by agent presets', + countUnit: 'plugins', switcherLabel: 'Choose the agent preset to inspect', presetOptionDefault: '{name} (default)', presetOptionBroken: '{name} (failed to load)', - globalTitle: 'Global', + globalTitle: 'Global plugins', globalSubtitle: 'Shared by the system and every session', presetProvidedDetail: 'Disabled globally; agent presets provide it per session', enabledIn: 'Enabled in', diff --git a/packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx b/packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx index 6a3702d7ae..69ce356a49 100644 --- a/packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx +++ b/packages/client/ui-settings-plugin-inventory/tests/components.client.spec.tsx @@ -92,7 +92,7 @@ describe('PluginInventorySettingsTab', () => { fireEvent.keyDown(document, { key: 'Escape' }) expect(screen.queryAllByRole('menuitem')).toHaveLength(0) expect(screen.getByText(en.presetSubtitle)).toBeTruthy() - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('6') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6') // Only the preset group lists rows while the global plane stays collapsed. expect(screen.getAllByRole('listitem')).toHaveLength(6) @@ -105,7 +105,7 @@ describe('PluginInventorySettingsTab', () => { expect(screen.queryByRole('img', { name: 'Not running' })).toBeNull() expect(globalToggle().getAttribute('aria-expanded')).toBe('false') - expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('7') + expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('7') expect(screen.getByText(`1 ${en.failedCountLabel}`)).toBeTruthy() // A preset row expands into its provenance facts. @@ -172,25 +172,25 @@ describe('PluginInventorySettingsTab', () => { } pickPreset('ptc') - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('3') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('3') fireEvent.click(screen.getAllByRole('button', { name: 'tool-bash, Enabled' })[0]!) // An unnamed preset labels provenance by its id. expect(screen.getByText(en.fromPreset).nextElementSibling?.textContent).toBe('ptc') pickPreset('坏预设 (failed to load)') expect(screen.getByRole('alert').textContent).toBe('the composition file is missing') - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('0') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('0') }) it('collapses the preset group until a search forces it open', async () => { const view = await renderReady() - const toggle = screen.getByRole('button', { name: en.presetSubtitle }) + const toggle = screen.getByRole('button', { name: en.presetTitle }) expect(toggle.getAttribute('aria-expanded')).toBe('true') fireEvent.click(toggle) expect(toggle.getAttribute('aria-expanded')).toBe('false') // The header keeps its count while the rows are folded away. - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('6') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('6') expect(view.container.querySelectorAll('[data-plugin-scope="preset"] li')).toHaveLength(0) fireEvent.change(screen.getByRole('searchbox', { name: en.search }), { target: { value: 'pwsh' } }) @@ -221,8 +221,8 @@ describe('PluginInventorySettingsTab', () => { fireEvent.change(search, { target: { value: 'tool-bash' } }) // Searching forces the collapsed global plane and drawer open. - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('1') - expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('1') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('1') + expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('1') expect(screen.getByText(en.presetEnabledTag)).toBeTruthy() expect(screen.queryByText(`1 ${en.failedCountLabel}`)).toBeNull() const hint = screen.getByText((text: string) => text.startsWith('2 more matches')) @@ -232,13 +232,13 @@ describe('PluginInventorySettingsTab', () => { // A match visible only in another preset keeps the pointer without rows. fireEvent.change(search, { target: { value: 'crashy' } }) - expect(view.container.querySelector('[data-preset-plugin-count]')?.textContent).toBe('0') + expect(view.container.querySelector('[data-preset-plugin-count]')?.getAttribute('data-preset-plugin-count')).toBe('0') expect(screen.getByText((text: string) => text.startsWith('1 more matches'))).toBeTruthy() expect(screen.queryByText(en.emptySearch)).toBeNull() // A match on a Loader entry id only reaches the global plane. fireEvent.change(search, { target: { value: '8a1b2c3d' } }) - expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('1') + expect(view.container.querySelector('[data-plugin-count]')?.getAttribute('data-plugin-count')).toBe('1') expect(screen.queryByText((text: string) => text.includes('more matches'))).toBeNull() fireEvent.change(search, { target: { value: 'not-a-plugin' } })