Просмотр исходного кода

fix(web): keep plugin cards in title order across switches

The Host lists selected bundles before the rest of the profile's
dependencies, so switching a bundle moved its card to the front or the
back of the sidebar page. The store now orders the views by the title a
person reads, and a card stays where it is whatever the Host answers.
Yichen Jiang 1 месяц назад
Родитель
Сommit
0d4b32f077

+ 2 - 2
packages/client/ui-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-plugin-manager/README.md
-README.md: 9e2f5616f0a2e3d20794c452af736b506d8af1a1
-README.zh.md: b0ad472987e1556d4ac574a58ea42e4a57a9d627
+README.md: 256354518e65ef68d4a0fcafcea02d1cafdab0c0
+README.zh.md: ee3c33ada9a40154545b10b3f55dd8ca9909b535

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

@@ -25,7 +25,7 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b
 <a id="use-this-package"></a>
 ## Use this package
 
-Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. Global configuration remains in the Settings **Plugins** section.
+Select **Plugins** in the sidebar. The page reads the inventory and the bundles through `api-remotes` when first opened; a Host without a managed profile shows the page as unavailable. Cards are listed by title, so switching a bundle on or off does not move its card. Global configuration remains in the Settings **Plugins** section.
 
 ### Installing a bundle
 

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

@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。全局配置仍在设置的**插件**分区中编辑。
+在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。卡片按标题排序,启停组合包不会挪动它的卡片。全局配置仍在设置的**插件**分区中编辑。
 
 ### 安装一个组合包
 

+ 13 - 1
packages/client/ui-plugin-manager/src/client/manager-store.ts

@@ -23,6 +23,7 @@ import type {
   ReadOnlyReason,
 } from '@deepseek-ai/dsh-api-remotes/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
+import { shortName } from './presentation.ts'
 
 /** What the last action left to say, shown as a toast; `seq` tells one showing from the next. */
 export type ManagerNotice =
@@ -276,6 +277,17 @@ export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]):
   }
 }
 
+/**
+ * The order the list shows packages in: by the title a person reads, so a
+ * card stays put when its bundle is switched, whatever order the Host answers in.
+ * @param packages - the Host's bundles as views.
+ * @returns the views sorted by title.
+ */
+export function sortPackages(packages: readonly PackageView[]): PackageView[] {
+  const label = (pkg: PackageView): string => pkg.title ?? shortName(pkg.name)
+  return [...packages].sort((a, b) => label(a).localeCompare(label(b)))
+}
+
 const IDLE_INSTALL: InstallState = {
   open: false, spec: '', phase: 'idle', inputError: null, subject: null, runs: [], detailsOpen: false,
   installed: null, restartRequired: false, failure: null, enabling: false,
@@ -443,7 +455,7 @@ export class PluginManagerController {
         }
         this.patch({
           status: 'ready',
-          packages: bundles.value.map(bundle => packageView(bundle, plugins.value)),
+          packages: sortPackages(bundles.value.map(bundle => packageView(bundle, plugins.value))),
         })
       } while (this.shouldRerun())
     } finally {

+ 15 - 1
packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts

@@ -6,7 +6,7 @@
 import { describe, expect, it, vi } from 'vitest'
 import type { BundleInfo, ChangeResult, ManagementError, PluginEntryId, PluginInfo, PluginInstallRequestId } from '@deepseek-ai/dsh-api-remotes/client'
 import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
-import { packageView, PluginManagerController, rowKey } from '../src/client/manager-store.ts'
+import { packageView, PluginManagerController, rowKey, sortPackages } from '../src/client/manager-store.ts'
 
 const ROW_ENTRY = 'include:sidebar' as PluginEntryId
 
@@ -108,6 +108,20 @@ describe('packageView', () => {
   })
 })
 
+describe('sortPackages', () => {
+  it('orders packages by the title a person reads, not by the Host order or enablement', async () => {
+    const plain = { enabled: true, installed: true, removable: true, rows: [], overrides: [] }
+    const zeta: BundleInfo = { ...plain, name: 'dsh-zeta' }
+    const alpha: BundleInfo = { ...plain, name: '@acme/dsh-alpha', title: 'Alpha tools', enabled: false }
+    const views = [zeta, BUNDLE, alpha].map(bundle => packageView(bundle, PLUGINS))
+    expect(sortPackages(views).map(pkg => pkg.name)).toEqual(['@acme/dsh-alpha', 'dsh-better-sidebar', 'dsh-zeta'])
+    // The store lists what it read in that order, whatever the Host's order.
+    const { state, controller } = bench({ listBundles: vi.fn(() => Promise.resolve(ok([zeta, BUNDLE, alpha]))) })
+    await controller.load()
+    expect(state().packages.map(pkg => pkg.name)).toEqual(['@acme/dsh-alpha', 'dsh-better-sidebar', 'dsh-zeta'])
+  })
+})
+
 describe('PluginManagerController', () => {
   it('starts idle, reads the inventory then the bundles and entries on first use, and folds concurrent loads', async () => {
     const gate = deferred<ReturnType<typeof ok<BundleInfo[]>>>()