diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml
index 496a456c70..dbb4836fb5 100644
--- a/packages/client/ui-plugin-manager/README.i18n.yaml
+++ b/packages/client/ui-plugin-manager/README.i18n.yaml
@@ -33,8 +33,8 @@
en: 5266d86638cc5cd7
zh: 398d710df9806cea
/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/page-tabs:
- en: d9eb9fdbccc39b3b
- zh: e2f2fa1857a07678
+ en: 76e0908903ab8fde
+ zh: ce6ae1cdd6aea971
/deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation:
en: 203168fc87e939bd
zh: 72d0ccd2c54f3f44
diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md
index 9fe0a6b19f..21d56214a4 100644
--- a/packages/client/ui-plugin-manager/README.md
+++ b/packages/client/ui-plugin-manager/README.md
@@ -98,7 +98,7 @@ A row's page exists only while a `plugins.row.config` entry names the row, so a
### Page tabs
-Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install(spec, registry?)` and `openPackage(name)`. `install` is for a spec the person already confirmed in the tab: the install dialog opens at its check screen with that spec, without the spec form, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here.
+Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install({ spec, registry?, title, version })` and `openPackage(name)`. `install` is for a version the person already confirmed in the tab: the install dialog opens at its check screen, without the spec form, names the plugin with `title` and `version` above the spec, which it shows in smaller monospace text, keeps that name on the progress and result screens, with the installed version once done, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here.
-----
diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md
index 7c1310b406..2d4e6f72ca 100644
--- a/packages/client/ui-plugin-manager/README.zh.md
+++ b/packages/client/ui-plugin-manager/README.zh.md
@@ -98,7 +98,7 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({
### 页面标签页
-其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install(spec, registry?)` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的 spec:安装对话框以该 spec 在检查界面打开,不显示 spec 表单;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。
+其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的版本:安装对话框在检查界面打开,不显示 spec 表单,在以较小等宽字显示的 spec 上方用 `title` 和 `version` 标明插件,并在进度和结果界面保持该名称,完成后显示已安装的版本;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。
-----
diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
index 36ddce9866..efb3fb04f7 100644
--- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
+++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
@@ -1017,6 +1017,15 @@ label.registryOption {
overflow-wrap: anywhere;
}
+.subjectSpec {
+ margin: 0;
+ font-family: var(--ds-font-family-code);
+ font-size: 12px;
+ line-height: 18px;
+ color: var(--dsw-alias-label-tertiary);
+ overflow-wrap: anywhere;
+}
+
.subjectDesc,
.subjectMeta {
margin: 0;
diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
index fb4b0f9459..77be2faf9f 100644
--- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
+++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
@@ -928,20 +928,45 @@ function failureText(failure: InstallState['failure'], t: Translate, install?: P
return failure.reason === '' ? t('installFailureGeneric') : failure.reason
}
-/** The package the install is about: its name, one-liner, and version, as the Host read them before installing. */
-function SubjectCard({ subject, t }: { readonly subject: InstallSubject; readonly t: Translate }): ReactNode {
- const title = subject.name ?? subject.spec
- const kindKey = SUBJECT_KIND_KEYS[subject.kind]
- const description = subject.description ?? (kindKey === undefined ? undefined : t(kindKey))
+interface SubjectLines {
+ readonly spec: string
+ readonly title: string
+ readonly description?: string | undefined
+ readonly version?: string | undefined
+ /** A confirmed version is named by its tab, so the spec follows as reference text. */
+ readonly showSpec: boolean
+}
+
+/** The package an install is about: its name, one-liner, version, and, when `showSpec` is set, its spec. */
+function SubjectBlock({ lines, t }: { readonly lines: SubjectLines; readonly t: Translate }): ReactNode {
return (
-
-
{title}
- {description === undefined ? null :
{description}
}
- {subject.version === undefined ? null :
{t('installVersion', { version: subject.version })}
}
+
+
{lines.title}
+ {lines.description === undefined ? null :
{lines.description}
}
+ {lines.version === undefined ? null :
{t('installVersion', { version: lines.version })}
}
+ {lines.showSpec ?
{lines.spec}
: null}
)
}
+/** The checked package as the Host read it before installing; a confirmed version keeps its tab's name and version. */
+function SubjectCard({ subject, confirmed, t }: {
+ readonly subject: InstallSubject
+ readonly confirmed: InstallState['confirmed']
+ readonly t: Translate
+}): ReactNode {
+ const kindKey = SUBJECT_KIND_KEYS[subject.kind]
+ return (
+
+ )
+}
+
/** Track one install field's composition, including Safari's 10ms post-composition Enter window. */
function useInstallComposition(active: boolean) {
const composition = useRef({ active: false, until: 0 })
@@ -1045,9 +1070,9 @@ function InstallDialog({
/>
)
}
- if ((phase === 'idle' || phase === 'checking') && install.confirmed === true) {
- // A confirmed spec has no form to return to: the dialog shows the spec with its check, a refusal with a retry, or,
- // after a cancelled run, the spec with Install.
+ if ((phase === 'idle' || phase === 'checking') && install.confirmed !== undefined) {
+ // A confirmed spec has no form to return to: the dialog names the plugin and version above the spec, with its
+ // check, a refusal with a retry, or, after a cancelled run, Install.
const checking = phase === 'checking'
const problem = install.inputError
return (
@@ -1065,7 +1090,7 @@ function InstallDialog({
)}
>
-
{install.spec}
+
{checking
?
{t('installChecking')}
: problem === null
@@ -1309,19 +1334,19 @@ function InstallDialog({
})
: null
// Another registry is worth offering only for a failure the Host laid at the one it asked.
- const changeable = phase === 'failed' && !approvable && install.failure?.failedAt === 'registry' && install.confirmed !== true
+ const changeable = phase === 'failed' && !approvable && install.failure?.failedAt === 'registry' && install.confirmed === undefined
const subject = install.subject
return (
- {phase === 'done' || (install.confirmed === true && !pending)
+ {phase === 'done' || (install.confirmed !== undefined && !pending)
?
: (
-
- {t(install.confirmed === true ? 'cancel' : pending ? 'installCancelAndEdit' : 'installEdit')}
+ {t(install.confirmed !== undefined ? 'cancel' : pending ? 'installCancelAndEdit' : 'installEdit')}
)}
{subject === null
? null
- : }
+ : }
{approvable
? (
diff --git a/packages/client/ui-plugin-manager/src/client/index.ts b/packages/client/ui-plugin-manager/src/client/index.ts
index dedc190351..548b5fe8e5 100644
--- a/packages/client/ui-plugin-manager/src/client/index.ts
+++ b/packages/client/ui-plugin-manager/src/client/index.ts
@@ -49,7 +49,7 @@ export type { ConfigLedger, OfficialItem } from './config-ledger.ts'
export type { PluginManagerFace } from './manager-store.ts'
export type { PluginManagerLocaleKey } from './locales.ts'
export type {
- ConfigPageForm, PluginActivationOwnerProps, PluginAddActionsProps, PluginTabProps,
+ ConfigPageForm, PluginActivationOwnerProps, PluginAddActionsProps, PluginTabInstall, PluginTabProps,
PluginConfigViewProps, PluginDetailProps, PluginPackageRef, PluginRowRef, PluginsSubject,
} from './slot-contract.ts'
diff --git a/packages/client/ui-plugin-manager/src/client/manager-store.ts b/packages/client/ui-plugin-manager/src/client/manager-store.ts
index 0a8dd4cf36..49bb36b67d 100644
--- a/packages/client/ui-plugin-manager/src/client/manager-store.ts
+++ b/packages/client/ui-plugin-manager/src/client/manager-store.ts
@@ -33,6 +33,7 @@ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
import type { LocalizedText, PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest'
import type { SettingsDescribeFace, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client'
import type { ConfigLedger } from './config-ledger.ts'
+import type { PluginTabInstall } from './slot-contract.ts'
import type { PluginTab } from './tab-ledger.ts'
import { shortName } from './presentation.ts'
@@ -199,8 +200,11 @@ export interface InstallState {
readonly spec: string
/** The spec form was reopened after switching away from a failed GitHub address. */
readonly mirrorRecovery?: boolean
- /** The spec came from a confirmed entry of another tab: the dialog shows no spec form, only its check and outcome. */
- readonly confirmed?: boolean
+ /**
+ * The spec came from a confirmed entry of another tab, with that entry's name and version: the dialog shows no spec
+ * form, only its check and outcome.
+ */
+ readonly confirmed?: { readonly title: string; readonly version: string }
/** The registries the Host configured, read when the dialog opens; null until the Host answered. */
readonly registries: PluginRegistries | null
/** The registry this install asks first: the one last used, else the Host's first. */
@@ -272,7 +276,7 @@ export function isInstallPending(phase: InstallState['phase']): boolean {
*/
export function githubRecoveryRegistry(install: InstallState): Registry | undefined {
// The mirror recovery reopens the spec form, which a confirmed spec does not have.
- if (install.confirmed === true || install.phase !== 'failed' || install.failure?.failedAt !== 'spec-host'
+ if (install.confirmed !== undefined || install.phase !== 'failed' || install.failure?.failedAt !== 'spec-host'
|| (install.failure.kind !== 'network' && install.failure.kind !== 'timeout')) return undefined
const host = install.subject?.host?.toLowerCase().split(':')[0]
if (host !== 'github.com' && !host?.endsWith('.github.com')) return undefined
@@ -338,12 +342,12 @@ export interface PluginManagerFace {
refresh: () => void
openInstall: () => void
/**
- * Open the install dialog with a spec the person confirmed elsewhere, such as a Discover entry, and start checking
- * it; the dialog shows no spec form. A hidden installation reopens instead.
- * @param spec - the exact install spec.
- * @param registry - the registry to ask first for this dialog only, never remembered; absent keeps the dialog's choice.
+ * Open the install dialog with a version the person confirmed elsewhere, such as a Discover entry, and start
+ * checking it; the dialog shows no spec form. A hidden installation reopens instead. The registry is asked first for
+ * this dialog only and never remembered.
+ * @param target - the exact spec, optional registry, and the name and version the dialog shows.
*/
- installFrom: (spec: string, registry?: string) => void
+ installFrom: (target: PluginTabInstall) => void
/** Hide immediately, abort a check, or request cancellation while retaining the Host-owned installation. */
closeInstall: () => void
editInstallSpec: (text: string) => void
@@ -613,11 +617,11 @@ export class PluginManagerController {
this.ctx.get('productAnalytics')?.track('plugin_add_button_click', {})
this.openDialog()
},
- installFrom: (spec, registry) => {
+ installFrom: ({ spec, registry, title, version }) => {
// A hidden installation reopens as it is; only one installation runs at a time.
if (!this.openDialog()) return
this.transientRegistry = registry !== undefined
- this.patchInstall({ spec, confirmed: true, ...registry === undefined ? {} : { registry: { kind: 'custom', url: registry } } })
+ this.patchInstall({ spec, confirmed: { title, version }, ...registry === undefined ? {} : { registry: { kind: 'custom', url: registry } } })
void this.runInstall()
},
closeInstall: () => {
diff --git a/packages/client/ui-plugin-manager/src/client/slot-contract.ts b/packages/client/ui-plugin-manager/src/client/slot-contract.ts
index 4494b3fde7..ff37603568 100644
--- a/packages/client/ui-plugin-manager/src/client/slot-contract.ts
+++ b/packages/client/ui-plugin-manager/src/client/slot-contract.ts
@@ -79,15 +79,26 @@ export interface PluginActivationOwnerProps {
readonly onOpenDetails: () => void
}
+/** An exact version a tab asks the Plugins page to install, with the text the install dialog shows for it. */
+export interface PluginTabInstall {
+ /** The install spec, such as `name@version` or a pinned `git+https` URL. */
+ readonly spec: string
+ /** The registry to ask first for this install only; absent keeps the dialog's registry choice. */
+ readonly registry?: string
+ /** Display name of the plugin, already localized. */
+ readonly title: string
+ /** The version being installed. */
+ readonly version: string
+}
+
/** Owner props of a page tab another plugin contributes, such as Discover. */
export interface PluginTabProps {
/**
- * Install an exact spec the person already confirmed: the page's install dialog opens at its check and progress
- * screens, without the spec form.
- * @param spec - the install spec, such as `name@version`.
- * @param registry - the registry to ask first for this install only; absent keeps the dialog's registry choice.
+ * Install an exact version the person already confirmed: the page's install dialog opens at its check and progress
+ * screens, without the spec form, and names the plugin and version above the spec.
+ * @param target - the spec, optional registry, and display text.
*/
- readonly install: (spec: string, registry?: string) => void
+ readonly install: (target: PluginTabInstall) => void
/**
* Switch to the Installed tab and open a bundle's page.
* @param packageName - npm package name of the bundle.
diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
index 058f5f3abc..be382f110b 100644
--- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
+++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
@@ -212,7 +212,7 @@ describe('PluginManagerPage', () => {
const tab = owner as PluginTabProps
return (
<>
- { tab.install('dsh-a@1.0.0', 'https://r.example/') }}>Discover install
+ { tab.install({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' }) }}>Discover install
{ tab.openPackage('dsh-better-sidebar') }}>Discover open
>
)
@@ -228,7 +228,7 @@ describe('PluginManagerPage', () => {
expect(document.querySelector('[data-plugin-group="bundles"]')).toBeNull()
expect(screen.queryByRole('button', { name: en.refresh })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Discover install' }))
- expect(actions.installFrom).toHaveBeenCalledWith('dsh-a@1.0.0', 'https://r.example/')
+ expect(actions.installFrom).toHaveBeenCalledWith({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' })
fireEvent.click(screen.getByRole('button', { name: 'Discover open' }))
expect(navigation.getSnapshot()).toEqual({ tab: 'installed', view: { kind: 'package', name: 'dsh-better-sidebar' } })
expect(document.querySelector('[data-plugin-detail="dsh-better-sidebar"]')).not.toBeNull()
@@ -270,10 +270,12 @@ describe('PluginManagerPage', () => {
expect(screen.getByRole('heading', { name: en.moreTitle, level: 1 })).toBeTruthy()
})
- it('shows a confirmed spec as a check and a refusal with retry, without the spec form or another registry', () => {
- const confirmed: InstallState = { ...IDLE_INSTALL, open: true, spec: 'dsh-a@1.0.0', confirmed: true, registries: REGISTRIES }
+ it('shows a confirmed version by name above its spec, as a check and a refusal with retry, without the spec form or another registry', () => {
+ const confirmed: InstallState = { ...IDLE_INSTALL, open: true, spec: 'dsh-a@1.0.0', confirmed: { title: 'Alpha', version: '1.0.0' }, registries: REGISTRIES }
const { actions, set } = renderTab({ install: { ...confirmed, phase: 'checking' } })
- expect(screen.getByRole('dialog', { name: en.installConfirmedTitle })).toBeTruthy()
+ const dialog = screen.getByRole('dialog', { name: en.installConfirmedTitle })
+ expect([...dialog.querySelectorAll('[data-install-subject] p')].map(line => line.textContent))
+ .toEqual(['Alpha', en.installVersion.replace('{version}', '1.0.0'), 'dsh-a@1.0.0'])
expect(screen.getByRole('status').textContent).toBe(en.installChecking)
expect(screen.queryByRole('textbox')).toBeNull()
expect(screen.queryByRole('button', { name: en.retry })).toBeNull()
@@ -311,6 +313,9 @@ describe('PluginManagerPage', () => {
})
expect(screen.getByText(en.installFailedTitle)).toBeTruthy()
expect(screen.queryByRole('button', { name: en.installUseGithubMirror })).toBeNull()
+ // A Git spec checks without a name or version; the confirmed name and version head the card, above the spec.
+ expect([...document.querySelectorAll('[data-install-subject] p')].map(line => line.textContent))
+ .toEqual(['Alpha', en.installSubjectGit, en.installVersion.replace('{version}', '1.0.0'), 'git+https://github.com/o/r.git#abc'])
set({
install: {
...confirmed, phase: 'running', requestId: 'r' as never,
diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
index 2944acc91b..c4592690da 100644
--- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
+++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
@@ -589,18 +589,18 @@ describe('PluginManagerController', () => {
.mockReturnValueOnce(gate.promise)
const { plugins, face, state, controller, started, track } = bench({ installBundle })
await controller.load()
- face.installFrom('dsh-new@1.0.0', 'https://r.example/')
+ face.installFrom({ spec: 'dsh-new@1.0.0', registry: 'https://r.example/', title: 'New', version: '1.0.0' })
expect(track).not.toHaveBeenCalledWith('plugin_add_button_click', {})
- expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0', confirmed: true, registry: { kind: 'custom', url: 'https://r.example/' } })
+ expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0', confirmed: { title: 'New', version: '1.0.0' }, registry: { kind: 'custom', url: 'https://r.example/' } })
await started()
expect(plugins.inspect).toHaveBeenCalledWith('dsh-new@1.0.0', { registry: 'https://r.example/' }, expect.any(AbortSignal))
face.closeInstall()
- face.installFrom('dsh-other@2.0.0')
+ face.installFrom({ spec: 'dsh-other@2.0.0', title: 'Other', version: '2.0.0' })
expect(state().install).toMatchObject({ open: true, spec: 'dsh-new@1.0.0' })
gate.resolve(ok({ ...APPLIED, bundle: 'dsh-new' }))
await vi.waitFor(() => { expect(state().install.requestId).toBeUndefined() })
face.closeInstall()
- face.installFrom('dsh-other@2.0.0')
+ face.installFrom({ spec: 'dsh-other@2.0.0', title: 'Other', version: '2.0.0' })
expect(state().install).toMatchObject({ open: true, spec: 'dsh-other@2.0.0', registry: { kind: 'offered', registry: null } })
await vi.waitFor(() => { expect(plugins.inspect).toHaveBeenLastCalledWith('dsh-other@2.0.0', { registry: null }, expect.any(AbortSignal)) })
await vi.waitFor(() => { expect(state().install.phase).toBe('done') })
diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml
index 2b12760fe5..4223a3e430 100644
--- a/packages/client/ui-plugin-market/README.i18n.yaml
+++ b/packages/client/ui-plugin-market/README.i18n.yaml
@@ -18,8 +18,8 @@
en: 2690b95e156cc473
zh: 0d9e897ad628f9dd
/deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation:
- en: cc9e8550a01ea847
- zh: eda18f5680df3e88
+ en: c56790cb58083f85
+ zh: a3d4ad4bad08e483
/deepseek-ai-dsh-client-ui-plugin-market/model-experience:
en: 08d25ab9cbdd7567
zh: 26e6e75c123951ee
diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md
index 02a88fd003..b89f158b64 100644
--- a/packages/client/ui-plugin-market/README.md
+++ b/packages/client/ui-plugin-market/README.md
@@ -41,7 +41,7 @@ Reading a market shows card skeletons. A failed read shows a short sentence with
## Understand the implementation
-The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install(spec, registry?)` and `openPackage(name)`. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork.
+The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another market starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork.
-----
diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md
index 69b0b32d64..c5d906e802 100644
--- a/packages/client/ui-plugin-market/README.zh.md
+++ b/packages/client/ui-plugin-market/README.zh.md
@@ -41,7 +41,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this-
## 理解实现
-本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install(spec, registry?)` 和 `openPackage(name)`。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。
+本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。
-----
diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css b/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css
index e91a063982..6ad459de5d 100644
--- a/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css
+++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.module.css
@@ -333,7 +333,7 @@
border-radius: 4px;
background: var(--dsw-alias-bg-module-platform);
box-decoration-break: clone;
- font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
+ font-family: var(--ds-font-family-code);
font-size: 12px;
}
diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx
index 011b8a119c..0941d101ba 100644
--- a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx
+++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx
@@ -589,7 +589,10 @@ export function DiscoverSection(props: DiscoverSectionProps): ReactNode {
{body}
{ setConfirming(null) }}
- onConfirm={({ version }) => { setConfirming(null); install(version.install.spec, version.install.registry) }} />
+ onConfirm={({ entry, version }) => {
+ setConfirming(null)
+ install({ ...version.install, title: entryTitle(entry, face), version: version.version })
+ }} />
)
}
diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx
index d7b64e9ea1..f0e4796ea0 100644
--- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx
+++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx
@@ -145,7 +145,7 @@ describe('DiscoverSection', () => {
expect(install).not.toHaveBeenCalled()
fireEvent.click(card('dsh-a').getByRole('button', { name: 'Install Alpha' }))
fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: en.install }))
- expect(install).toHaveBeenCalledWith('dsh-a@1.0.0', 'https://r.example/')
+ expect(install).toHaveBeenCalledWith({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' })
expect(screen.queryByRole('dialog')).toBeNull()
for (const [name, label] of [['dsh-b', en.statusInstalled], ['dsh-c', en.statusUpdate], ['dsh-f', en.statusProvided]] as const) {
fireEvent.click(card(name).getByRole('button', { name: `View ${name} in Installed` }))
@@ -346,7 +346,7 @@ describe('DiscoverSection', () => {
const dialog = within(screen.getByRole('dialog'))
expect(dialog.getByText('dsh-v@3.0.0-rc.1')).toBeTruthy()
fireEvent.click(dialog.getByRole('button', { name: en.install }))
- expect(install).toHaveBeenCalledWith('dsh-v@3.0.0-rc.1', undefined)
+ expect(install).toHaveBeenCalledWith({ spec: 'dsh-v@3.0.0-rc.1', title: 'dsh-v', version: '3.0.0-rc.1' })
})
it('marks exempted versions and offers no version of a bundle DSH provides', () => {
diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
index 399cbc7c98..4145c585ef 100644
--- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
+++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
@@ -2205,7 +2205,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.add.actions\', () => ctx.slots.register(\n { name: \'plugins.add.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:107',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:118',
},
{
key: 'plugins.bundle.activation',
@@ -2245,7 +2245,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.activation\', () => ctx.slots.register(\n { name: \'plugins.bundle.activation\', key: \'
\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:115',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:126',
},
{
key: 'plugins.bundle.config',
@@ -2288,7 +2288,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.config\', () => ctx.slots.register(\n { name: \'plugins.bundle.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:131',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:142',
},
{
key: 'plugins.detail.actions',
@@ -2340,7 +2340,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.actions\', () => ctx.slots.register(\n { name: \'plugins.detail.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:145',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:156',
},
{
key: 'plugins.detail.badge',
@@ -2392,7 +2392,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.badge\', () => ctx.slots.register(\n { name: \'plugins.detail.badge\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:150',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:161',
},
{
key: 'plugins.detail.section',
@@ -2444,7 +2444,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.section\', () => ctx.slots.register(\n { name: \'plugins.detail.section\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:157',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:168',
},
{
key: 'plugins.item',
@@ -2501,7 +2501,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.item\', () => ctx.slots.register(\n { name: \'plugins.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:125',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:136',
},
{
key: 'plugins.row.config',
@@ -2541,7 +2541,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.row.config\', () => ctx.slots.register(\n { name: \'plugins.row.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:139',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:150',
},
{
key: 'plugins.tab',
@@ -2570,9 +2570,11 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
},
],
ownerProps: [
- '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact spec the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form.\n * @param spec - the install spec, such as `name@version`.\n * @param registry - the registry to ask first for this install only; absent keeps the dialog\'s registry choice.\n */\n readonly install: (spec: string, registry?: string) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}',
+ '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact version the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form, and names the plugin and version above the spec.\n * @param target - the spec, optional registry, and display text.\n */\n readonly install: (target: PluginTabInstall) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}',
+ ],
+ ownerPropsReferences: [
+ 'PluginTabInstall',
],
- ownerPropsReferences: [],
standardProps: [
'useRemoteAccess: (action: RemoteEndpoint) => boolean',
'useResource: UseResource',
@@ -2593,7 +2595,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.tab\', () => ctx.slots.register(\n { name: \'plugins.tab\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:113',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:124',
},
{
key: 'rightbar',