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

fix(web): use installation records for plugin management

Yichen Jiang 1 неделя назад
Родитель
Сommit
17ce0910c4

+ 1 - 1
apps/web/tests/scaffold.ts

@@ -738,7 +738,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
       // Provenance is available before any configuration entry activates.
       const readProfile = (): Profile => loadProfile('dsh', 'scaffold', INSTALL_ANCHOR, harnessHome)
       const profile = readProfile()
-      const compose = (current: Profile): ComposedStack => composeProfileStack('web e2e scaffold', current.layers, [
+      const compose = (current: Profile): ComposedStack => composeProfileStack(current.layers, [
         { label: 'scaffold', patches },
         { label: current.patchPath, patches: current.patches },
       ])

+ 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: c34f69ba170a147f6313c21354c9e5667ac6d709
-README.zh.md: 387ca469f17f6978340fc8d424d943b5bd6757a2
+README.md: 5c2346183e79049433bedc4cfbc87f96e13da752
+README.zh.md: 50c428417d36a20a16d22b234d57aca059a6cf71

+ 2 - 0
packages/client/ui-plugin-manager/README.md

@@ -52,6 +52,8 @@ The page separates enablement, running phase and current issues. A missing servi
 <a id="understand-the-implementation"></a>
 ## Understand the implementation
 
+Package management uses the profile’s dependency records: installed bundles can be toggled and installed packages removed; installation-owned bundles remain locked. This distinction does not select startup failure policy.
+
 <details>
 <summary>Implementation internals — click to expand</summary>
 

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

@@ -52,6 +52,8 @@ kind: "package-reference"
 <a id="understand-the-implementation"></a>
 ## 理解实现
 
+插件管理依据 profile 的依赖记录:已安装组合包可启停,已安装包可移除,随安装提供的组合包保持锁定。这一区分不决定启动失败策略。
+
 <details>
 <summary>实现细节——点击展开</summary>
 

+ 5 - 5
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -366,7 +366,7 @@ function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: {
   readonly onSetEnabled: (enabled: boolean) => void
 }): ReactNode {
   if (pkg.kind !== 'bundle') return null
-  const builtin = pkg.trust === 'builtin'
+  const builtin = !pkg.installed
   return (
     <Switch
       checked={pkg.enabled}
@@ -392,7 +392,7 @@ function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen,
 }): ReactNode {
   const [addMenu, setAddMenu] = useState(false)
   const title = pkg.title ?? shortName(pkg.name)
-  const builtin = pkg.trust === 'builtin'
+  const builtin = !pkg.installed
   const status = cardStatus(pkg)
   const addable = pkg.addable
   const [single] = addable
@@ -478,14 +478,14 @@ function PackageDetail({
 }): ReactNode {
   const title = pkg.title ?? shortName(pkg.name)
   const bundle = pkg.kind === 'bundle'
-  const builtin = pkg.trust === 'builtin'
+  const builtin = !pkg.installed
   const status = cardStatus(pkg)
   const retryable = bundle && pkg.enabled && (pkg.status === 'failed' || pkg.status === 'partial')
-  const removable = pkg.installed && !builtin
+  const removable = pkg.installed
   const affectedIssues = (pkg.issues ?? []).filter(issue => !pkg.rows.some(row => row.entryId === issue.entryId))
   // A row's switch acts at once only on an external pack composed on a
   // profile that applies patches while it runs; elsewhere the rows stay read-only.
-  const switchable = bundle && !builtin && pkg.enabled && pkg.liveReload
+  const switchable = bundle && pkg.installed && pkg.enabled && pkg.liveReload
   return (
     <div className={css.detail} data-plugin-detail={pkg.name}>
       <button type="button" className={css.crumb} aria-label={t('backToList')} onClick={onBack}>

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

@@ -100,7 +100,7 @@ export function presetRowCopy(
     return {
       title: addable?.title ?? pkg.title ?? shortName(pkg.name),
       ...pkg.description === undefined ? {} : { description: pkg.description },
-      local: pkg.trust === 'external',
+      local: pkg.installed,
     }
   }
   const harness = harnessCopy(t, row.moduleName, rowIdOf(row.entryId))

+ 12 - 14
packages/client/ui-plugin-manager/tests/components.client.spec.tsx

@@ -22,8 +22,6 @@ function pkg(overrides: Partial<PluginPackageView> = {}): PluginPackageView {
     name: 'dsh-better-sidebar',
     version: '0.16.0',
     kind: 'bundle',
-    trust: 'external',
-    stage: 'runtime',
     installed: true,
     enabled: true,
     status: 'running',
@@ -154,15 +152,15 @@ describe('PluginManagerPage', () => {
   })
 
   it('lists every package as a card, tags only restarts, problems, and built-ins, and names what waits for a restart', () => {
-    const { version: _unversioned, ...firstPartyPackage } = pkg({
-      name: '@deepseek-ai/dsh-bundle-first-party', title: 'First party', trust: 'builtin', stage: 'boot',
+    const { version: _unversioned, ...bundledPackage } = pkg({
+      name: '@deepseek-ai/dsh-bundle-first-party', title: 'First party', installed: false,
     })
     const { actions } = renderTab({
       packages: [
         pkg({ description: 'A sidebar.' }),
-        firstPartyPackage,
-        pkg({ name: 'unknown', trust: 'builtin', kind: 'plugin', status: 'plain' }),
-        pkg({ name: 'builtin-lib', trust: 'builtin', kind: 'unknown', status: 'plain' }),
+        bundledPackage,
+        pkg({ name: 'unknown', installed: false, kind: 'plugin', status: 'plain' }),
+        pkg({ name: 'builtin-lib', installed: false, kind: 'unknown', status: 'plain' }),
         pkg({ name: 'broken-bundle', enabled: false, status: 'not-enableable', reason: 'foreign cordis' }),
         pkg({ name: 'dsh-tool-foo', kind: 'plugin', status: 'plain' }),
         pkg({ name: 'some-lib', kind: 'unknown', status: 'plain' }),
@@ -180,9 +178,9 @@ describe('PluginManagerPage', () => {
     // No kind tag, no running or off tag: the switch says that.
     expect(screen.getByText(en.statusProblem)).toBeTruthy()
     expect(screen.getAllByText(en.statusRestart)).toHaveLength(2)
-    expect(screen.getAllByText(en.builtinTag)).toHaveLength(3)
+    expect(screen.getAllByText(en.builtinTag)).toHaveLength(4)
     expect(screen.getByText('A sidebar.')).toBeTruthy()
-    expect(document.querySelectorAll('[data-tone]')).toHaveLength(6)
+    expect(document.querySelectorAll('[data-tone]')).toHaveLength(7)
 
     const sidebar = screen.getByRole('switch', { name: 'Enable better-sidebar' }) as HTMLButtonElement
     expect(sidebar.getAttribute('aria-checked')).toBe('true')
@@ -191,9 +189,9 @@ describe('PluginManagerPage', () => {
     expect(screen.getByRole('switch', { name: 'Enable broken-bundle' })).toHaveProperty('disabled', true)
     expect(screen.getByRole('switch', { name: 'Enable off-bundle' }).getAttribute('aria-checked')).toBe('false')
     // A built-in pack keeps a locked switch in the same list; a built-in plugin or library has none.
-    const firstParty = screen.getByRole('switch', { name: 'Enable First party' }) as HTMLButtonElement
-    expect(firstParty.disabled).toBe(true)
-    expect(firstParty.title).toBe(en.builtinLocked)
+    const bundled = screen.getByRole('switch', { name: 'Enable First party' }) as HTMLButtonElement
+    expect(bundled.disabled).toBe(true)
+    expect(bundled.title).toBe(en.builtinLocked)
     expect(screen.queryByRole('switch', { name: 'Enable unknown' })).toBeNull()
     expect(screen.queryByRole('switch', { name: 'Enable tool-foo' })).toBeNull()
 
@@ -237,7 +235,7 @@ describe('PluginManagerPage', () => {
         pkg({ name: 'no-rows', enabled: false, status: 'disabled' }),
         pkg({ name: 'dsh-tool-foo', kind: 'plugin', status: 'plain' }),
         pkg({
-          name: '@deepseek-ai/dsh-core-broken', title: 'Core', trust: 'builtin', status: 'failed',
+          name: '@deepseek-ai/dsh-core-broken', title: 'Core', installed: false, status: 'failed',
           rows: [{ entryId: 'include:core', rowId: 'core', moduleName: '@deepseek-ai/dsh-core-broken', enabled: true, phase: 'active' }],
         }),
       ],
@@ -333,7 +331,7 @@ describe('PluginManagerPage', () => {
         pkg({ name: 'many', rows: many }),
         pkg({ name: 'frozen', liveReload: false, rows: [{ entryId: 'include:frozen', rowId: 'frozen', moduleName: 'frozen', ...userOff }] }),
         pkg({ name: 'parked', enabled: false, status: 'disabled', rows: [{ entryId: 'include:parked', rowId: 'parked', moduleName: 'parked', ...userOff }] }),
-        pkg({ name: '@deepseek-ai/dsh-core', title: 'Core', trust: 'builtin', rows: [{ entryId: 'include:core', rowId: 'core', moduleName: '@deepseek-ai/dsh-core', ...userOff }] }),
+        pkg({ name: '@deepseek-ai/dsh-core', title: 'Core', installed: false, rows: [{ entryId: 'include:core', rowId: 'core', moduleName: '@deepseek-ai/dsh-core', ...userOff }] }),
       ],
     })
     const target = { kind: 'global' } as const

+ 0 - 2
packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts

@@ -12,8 +12,6 @@ const BUNDLE: PluginPackageView = {
   name: 'dsh-better-sidebar',
   version: '0.16.0',
   kind: 'bundle',
-  trust: 'external',
-  stage: 'runtime',
   installed: true,
   enabled: false,
   status: 'disabled',

+ 0 - 2
packages/client/ui-plugin-manager/tests/preset-section.client.spec.tsx

@@ -29,8 +29,6 @@ function pkg(overrides: Partial<PluginPackageView> = {}): PluginPackageView {
     name: 'dsh-tool-foo',
     version: '0.1.0',
     kind: 'plugin',
-    trust: 'external',
-    stage: 'runtime',
     installed: true,
     enabled: false,
     status: 'plain',