Browse Source

refactor(web): off components of a pack as chips grouped by reason

Only a failing component earns a line of its own; the ones a pack
switches off by design — base switches off dozens — sit as dashed chips
under why they are off, one group per reason.
Yichen Jiang 1 month ago
parent
commit
ce9337db4e

+ 2 - 2
packages/client/ui-settings-plugin-manager/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-manager/README.md
-README.md: 5db176df6151a5bfe000647be5b89565951a000c
-README.zh.md: ce99a9e2e460bae967eea92fa470c91ffea55f6f
+README.md: 59fb0384db26444dd75728217b2351644df71639
+README.zh.md: 39869920e504c28aff28ec13f9fee266891ba12c

File diff suppressed because it is too large
+ 0 - 1
packages/client/ui-settings-plugin-manager/README.md


+ 1 - 1
packages/client/ui-settings-plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-client-ui-settings-plugin-manager` 向 Web 设置的「插件」分区贡献**插件管理**标签页。该标签页在首次被选择时通过 `ctx.remote.plugins.list()` 读取 profile 的包、通过 `ctx.remote.pluginInventory.list()` 读取预设组合,并在每次操作之后、每个转发的 `plugins/changed` 事件之后重新读取,因此从 CLI 或另一个浏览器做出的改动无需手动刷新即可显示。页面只说两个名词但不把它们显示出来:*插件包*(bundle)整体启停、对所有会话生效;*插件*(插件模块)经**加入到…**加入某个预设或所有会话。**已安装**组为每个包列出一张卡片,带显示名与一句话描述;插件包带开关,插件带**加入到…**菜单,内置插件包带*内置*标记与锁定的开关,只有需重启或异常时卡片才打标签。展开的卡片显示版本、来源、插件包的组件——一行计数,然后只列未启用或异常的组件并注明原因,其余作为标签收在**显示全部**之后并可筛选——以及**卸载**;entry id、模块名、种类与探针事实不出现在页面上。预设的组合不在这个标签页上:本包把它作为**能力**段贡献到 Agent 预设分区里该预设的详情页——同样的卡片,harness 模块按标签页字典命名,自己装的模块按 manifest 命名并带*本地*标记,每张卡片一个开关,用户添加的行带**删除**,段头有**添加**菜单。安装对话框接受包名、本地路径或 Git 地址,把 pnpm 输出收在失败时自动展开的折叠里,并按包用一句话说明结果。有依赖方的停用与每次卸载都要等待一次确认,确认框点名还在使用该包的地方。没有 profile runtime 时标签页声明自己不可用且不提供任何操作。
+`dsh-client-ui-settings-plugin-manager` 向 Web 设置的「插件」分区贡献**插件管理**标签页。该标签页在首次被选择时通过 `ctx.remote.plugins.list()` 读取 profile 的包、通过 `ctx.remote.pluginInventory.list()` 读取预设组合,并在每次操作之后、每个转发的 `plugins/changed` 事件之后重新读取,因此从 CLI 或另一个浏览器做出的改动无需手动刷新即可显示。页面只说两个名词但不把它们显示出来:*插件包*(bundle)整体启停、对所有会话生效;*插件*(插件模块)经**加入到…**加入某个预设或所有会话。**已安装**组为每个包列出一张卡片,带显示名与一句话描述;插件包带开关,插件带**加入到…**菜单,内置插件包带*内置*标记与锁定的开关,只有需重启或异常时卡片才打标签。展开的卡片显示版本、来源、插件包的组件——一行计数,异常的组件一行一个并附失败信息,未启用的组件按原因分组显示为标签,其余作为标签收在**显示全部**之后并可筛选——以及**卸载**;entry id、模块名、种类与探针事实不出现在页面上。预设的组合不在这个标签页上:本包把它作为**能力**段贡献到 Agent 预设分区里该预设的详情页——同样的卡片,harness 模块按标签页字典命名,自己装的模块按 manifest 命名并带*本地*标记,每张卡片一个开关,用户添加的行带**删除**,段头有**添加**菜单。安装对话框接受包名、本地路径或 Git 地址,把 pnpm 输出收在失败时自动展开的折叠里,并按包用一句话说明结果。有依赖方的停用与每次卸载都要等待一次确认,确认框点名还在使用该包的地方。没有 profile runtime 时标签页声明自己不可用且不提供任何操作。
 
 ## 目录
 

+ 13 - 0
packages/client/ui-settings-plugin-manager/src/client/PluginManagerSettingsTab.module.css

@@ -534,9 +534,16 @@
 
 .partWhy {
   font-size: 12px;
+  line-height: 18px;
   color: var(--dsw-alias-label-tertiary);
 }
 
+.partGroup {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
 .partFailure {
   grid-column: 2 / -1;
   margin: 0;
@@ -588,6 +595,12 @@
   font-size: 11.5px;
 }
 
+.chip[data-state='off'] {
+  border: 0.5px dashed var(--dsw-alias-border-l4);
+  background: transparent;
+  color: var(--dsw-alias-label-tertiary);
+}
+
 .statusDot {
   display: inline-block;
   width: 7px;

+ 34 - 17
packages/client/ui-settings-plugin-manager/src/client/PluginManagerSettingsTab.tsx

@@ -85,16 +85,32 @@ function partsSummary(rows: readonly RowView[], t: Translate): string {
   ].join(' · ')
 }
 
+/** One component as a chip: its declared id, with its fiber phase when it has one. */
+function PartChip({ row, off, t }: { readonly row: RowView; readonly off: boolean; readonly t: Translate }): ReactNode {
+  return (
+    <li className={css.chip} data-plugin-row={row.entryId} {...off ? { 'data-state': 'off' } : {}}>
+      {row.phase === null ? null : <PhaseDot phase={row.phase} t={t} />}
+      {row.rowId}
+    </li>
+  )
+}
+
 /**
- * The components of one plugin pack: a count line, the components that are
- * off or failing with why, and the rest as chips behind **Show all** with a
- * filter — a pack like base carries close to a hundred rows, of which the
- * few that are not running are the ones worth a line each.
+ * The components of one plugin pack: a count line; the failing components,
+ * one line each with the failure; the off ones as chips under why they are
+ * off; and the rest as chips behind **Show all** with a filter. A pack like
+ * base carries close to a hundred rows and switches dozens off by design,
+ * so only a failure earns a line of its own.
  */
 function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly t: Translate }): ReactNode {
   const [open, setOpen] = useState(false)
   const [filter, setFilter] = useState('')
-  const exceptions = rows.filter(row => isFailedRow(row) || !row.enabled)
+  const failed = rows.filter(isFailedRow)
+  const off = rows.filter(row => !isFailedRow(row) && !row.enabled)
+  const offGroups = [
+    { key: 'partDisabledByUser' as const, rows: off.filter(row => row.disabledBy === 'user') },
+    { key: 'partDisabledByComposition' as const, rows: off.filter(row => row.disabledBy !== 'user') },
+  ].filter(group => group.rows.length > 0)
   const rest = rows.filter(row => !isFailedRow(row) && row.enabled)
   const query = filter.trim().toLowerCase()
   const shown = query === '' ? rest : rest.filter(row => row.rowId.toLowerCase().includes(query))
@@ -117,22 +133,28 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
           )}
       </div>
       {rows.length === 0 ? <p className={css.status}>{t('partsEmpty')}</p> : null}
-      {exceptions.length === 0
+      {failed.length === 0
         ? null
         : (
           <ul className={css.partExceptions}>
-            {exceptions.map(row => (
+            {failed.map(row => (
               <li key={row.entryId} className={css.partException} data-plugin-row={row.entryId}>
-                <span className={css.statusDot} data-phase={isFailedRow(row) ? 'failed' : 'off'} aria-hidden="true" />
+                <span className={css.statusDot} data-phase="failed" aria-hidden="true" />
                 <span className={css.partId}>{row.rowId}</span>
-                {isFailedRow(row)
-                  ? <Tag kind="problem">{t('rowStateFailed')}</Tag>
-                  : <span className={css.partWhy}>{t(row.disabledBy === 'user' ? 'partDisabledByUser' : 'partDisabledByComposition')}</span>}
+                <Tag kind="problem">{t('rowStateFailed')}</Tag>
                 {row.failure === undefined ? null : <p className={css.partFailure}>{row.failure.message}</p>}
               </li>
             ))}
           </ul>
         )}
+      {offGroups.map(group => (
+        <div key={group.key} className={css.partGroup}>
+          <span className={css.partWhy}>{t('partsGroupLabel', { reason: t(group.key), count: String(group.rows.length) })}</span>
+          <ul className={css.chips}>
+            {group.rows.map(row => <PartChip key={row.entryId} row={row} off t={t} />)}
+          </ul>
+        </div>
+      ))}
       {open
         ? (
           <div className={css.partsAll}>
@@ -148,12 +170,7 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
               ? <p className={css.status}>{t('partsFilterEmpty')}</p>
               : (
                 <ul className={css.chips}>
-                  {shown.map(row => (
-                    <li key={row.entryId} className={css.chip} data-plugin-row={row.entryId}>
-                      {row.phase === null ? null : <PhaseDot phase={row.phase} t={t} />}
-                      {row.rowId}
-                    </li>
-                  ))}
+                  {shown.map(row => <PartChip key={row.entryId} row={row} off={false} t={t} />)}
                 </ul>
               )}
           </div>

+ 2 - 0
packages/client/ui-settings-plugin-manager/src/client/locales.ts

@@ -47,6 +47,7 @@ export const zh = {
   partsCollapse: '收起',
   partsFilter: '筛选组件',
   partsFilterEmpty: '没有匹配的组件。',
+  partsGroupLabel: '{reason}({count})',
   partDisabledByUser: '已停用 · 你关闭的',
   partDisabledByComposition: '未启用 · 插件包自身关闭',
   rowPhasePending: '等待依赖',
@@ -230,6 +231,7 @@ export const en = {
   partsCollapse: 'Collapse',
   partsFilter: 'Filter components',
   partsFilterEmpty: 'No component matches.',
+  partsGroupLabel: '{reason} ({count})',
   partDisabledByUser: 'Off · switched off by you',
   partDisabledByComposition: 'Not enabled · off in the pack itself',
   rowPhasePending: 'Waiting for dependencies',

+ 7 - 4
packages/client/ui-settings-plugin-manager/tests/components.client.spec.tsx

@@ -204,12 +204,14 @@ describe('PluginManagerSettingsTab', () => {
     expect(screen.getByText(`${en.reasonLabel}: one row failed`)).toBeTruthy()
     expect(screen.getByText('0.16.0')).toBeTruthy()
     expect(screen.getByText(en.sourceLocal)).toBeTruthy()
-    // The count line, then only the components that are off or failing, each
-    // with why; the running ones wait behind Show all.
+    // The count line; the failing components one line each; the off ones as
+    // chips under why they are off; the running ones wait behind Show all.
     expect(screen.getByText('6 total · 1 running · 2 off · 2 failed')).toBeTruthy()
     expect(document.querySelector('[data-plugin-row="include:better-sidebar"]')).toBeNull()
-    expect(document.querySelector('[data-plugin-row="include:off"]')?.textContent).toContain(en.partDisabledByUser)
-    expect(document.querySelector('[data-plugin-row="include:gated"]')?.textContent).toContain(en.partDisabledByComposition)
+    expect(screen.getByText(`${en.partDisabledByUser} (1)`)).toBeTruthy()
+    expect(screen.getByText(`${en.partDisabledByComposition} (1)`)).toBeTruthy()
+    expect(document.querySelector('[data-plugin-row="include:off"]')?.getAttribute('data-state')).toBe('off')
+    expect(document.querySelector('[data-plugin-row="include:gated"]')?.textContent).toBe('gated')
     expect(document.querySelector('[data-plugin-row="include:crash"]')?.textContent).toContain(en.rowStateFailed)
     expect(document.querySelector('[data-plugin-row="include:flaky"]')?.textContent).toBe(`flaky${en.rowStateFailed}`)
     expect(screen.getByText('boom')).toBeTruthy()
@@ -218,6 +220,7 @@ describe('PluginManagerSettingsTab', () => {
     // A component shows the id its pack declared, not the tree-wide entry id.
     expect(document.querySelector('[data-plugin-row="include:better-sidebar"]')?.textContent).toContain('better-sidebar')
     expect(document.querySelector('[data-plugin-row="include:idle"]')?.textContent).toBe('idle')
+    expect(document.querySelector('[data-plugin-row="include:idle"]')?.getAttribute('data-state')).toBeNull()
     const filter = screen.getByRole('searchbox', { name: en.partsFilter })
     fireEvent.change(filter, { target: { value: 'nope' } })
     expect(screen.getByText(en.partsFilterEmpty)).toBeTruthy()

Some files were not shown because too many files changed in this diff