From 0b3256014cc96dd3651ba784a7508d3879ade086 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:57:03 -0700 Subject: [PATCH 1/3] fix(web): center plugin installation example buttons --- .../ui-plugin-manager/src/client/PluginManagerPage.module.css | 4 ++++ 1 file changed, 4 insertions(+) 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 09810fe85b..90e7684f37 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -599,6 +599,10 @@ padding: 8px 0; } +.guideItem > button { + align-self: center; +} + .guideIndex { display: inline-flex; flex: none; From 473fd15101fb473cc7f8db25556eabd3b844116b Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Sun, 27 Sep 2026 07:18:09 -0700 Subject: [PATCH 2/3] fix(web): clarify plugin management guidance and warnings --- .../plugin-manager/live-enabled.expected.md | 5 +- .../plugin-manager/loading.expected.md | 5 +- .../plugin-manager/manager.expected.md | 5 +- apps/web/tests/plugin-manager.e2e.ts | 40 +++++++++- .../client/ui-plugin-manager/README.i18n.yaml | 8 +- packages/client/ui-plugin-manager/README.md | 4 +- .../client/ui-plugin-manager/README.zh.md | 4 +- .../src/client/PluginManagerPage.module.css | 80 ++++++++++++++----- .../src/client/PluginManagerPage.tsx | 74 ++++++++++++++--- .../ui-plugin-manager/src/client/locales.ts | 16 +++- .../ui-sidebar-browser/README.i18n.yaml | 4 +- packages/client/ui-sidebar-browser/README.md | 2 +- .../client/ui-sidebar-browser/README.zh.md | 2 +- .../src/client/view/Browser.module.css | 4 +- 14 files changed, 192 insertions(+), 61 deletions(-) diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index a6d04f4f70..48410d3d95 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -1,5 +1,6 @@ -- heading "插件" [level=1] -- paragraph: 添加和管理插件 +- heading "插件管理" [level=1] +- text: 安装、启用和配置插件 +- button "内置插件在哪里" - button "刷新" - button "添加插件" - heading "官方" [level=3] diff --git a/apps/web/tests/expected/plugin-manager/loading.expected.md b/apps/web/tests/expected/plugin-manager/loading.expected.md index 467153a406..3529c1c300 100644 --- a/apps/web/tests/expected/plugin-manager/loading.expected.md +++ b/apps/web/tests/expected/plugin-manager/loading.expected.md @@ -1,5 +1,6 @@ -- heading "插件" [level=1] -- paragraph: 添加和管理插件 +- heading "插件管理" [level=1] +- text: 安装、启用和配置插件 +- button "内置插件在哪里" - button "刷新" [disabled] - button "添加插件" [disabled] - status "正在读取插件…" diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 8e523514b2..d031327b16 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -1,5 +1,6 @@ -- heading "插件" [level=1] -- paragraph: 添加和管理插件 +- heading "插件管理" [level=1] +- text: 安装、启用和配置插件 +- button "内置插件在哪里" - button "刷新" - button "添加插件" - heading "官方" [level=3] diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts index b7c375ec7e..88ff001766 100644 --- a/apps/web/tests/plugin-manager.e2e.ts +++ b/apps/web/tests/plugin-manager.e2e.ts @@ -85,7 +85,7 @@ describe('web e2e: plugin manager', () => { while (await panel.getByRole('button', { name: /^返回/ }).count() > 0) { await panel.getByRole('button', { name: /^返回/ }).first().click() } - await panel.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 10_000 }) + await panel.getByRole('heading', { name: '插件管理', exact: true }).waitFor({ timeout: 10_000 }) return panel } @@ -123,9 +123,10 @@ describe('web e2e: plugin manager', () => { expect(await skeleton.getAttribute('role')).toBe('status') expect(await skeleton.getAttribute('aria-label')).toBe('正在读取插件…') expect(await panel.getAttribute('aria-busy')).toBe('true') - const actions = panel.locator(':scope > header button') + const actions = panel.locator(':scope > header > div:last-child button') expect(await actions.count()).toBe(2) for (const action of await actions.all()) expect(await action.isDisabled()).toBe(true) + expect(await panel.getByRole('button', { name: '内置插件在哪里' }).isEnabled()).toBe(true) const loadingAria = await captureStableAria(probe, '[data-plugin-panel]', scaffold.workspaceCwd) if (aria === '') aria = loadingAria else expect(loadingAria).toBe(aria) @@ -371,6 +372,27 @@ describe('web e2e: plugin manager', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-list')) const panel = await openPluginsPanel() + const info = panel.getByRole('button', { name: '内置插件在哪里' }) + await info.hover() + const hoverHelp = page.getByText('内置插件列表及运行状态可在「设置 → 内置插件」中查看', { exact: true }) + await hoverHelp.waitFor() + expect(await page.getByRole('tooltip').count()).toBe(0) + await hoverHelp.hover() + expect(await hoverHelp.isVisible()).toBe(true) + await panel.getByRole('heading', { name: '插件管理', exact: true }).hover() + await hoverHelp.waitFor({ state: 'hidden' }) + await info.focus() + await page.keyboard.press('Enter') + const help = page.getByRole('dialog', { name: '内置插件在哪里' }) + await help.waitFor() + expect(await help.textContent()).toContain('设置 → 内置插件') + await page.keyboard.press('Escape') + expect(await help.count()).toBe(0) + expect(await info.evaluate(element => element === document.activeElement)).toBe(true) + await info.click() + await panel.getByRole('heading', { name: '插件管理', exact: true }).click() + expect(await help.count()).toBe(0) + await panel.getByRole('button', { name: '查看 @fixture/bundle', exact: true }).waitFor({ timeout: 20_000 }) const toggle = panel.getByRole('switch', { name: '启用 @fixture/bundle' }) expect(await toggle.getAttribute('aria-checked')).toBe('false') @@ -760,6 +782,20 @@ describe('web e2e: plugin manager', () => { const field = dialog.getByRole('textbox', { name: '包名或地址' }) const install = dialog.getByRole('button', { name: '安装', exact: true }) expect(await install.isDisabled()).toBe(true) + expect(await dialog.getByRole('note').textContent()).toContain('请确认插件来源可信') + await dialog.getByRole('button', { name: '插件安装引导和示例' }).click() + for (const [example, hint] of [ + ['https://github.com/author/dsh-plugin', '请替换为实际的 Git 仓库地址'], + ['/Users/name/my-plugin', '请替换为本机插件目录的实际路径'], + ]) { + await dialog.getByRole('button', { name: `填入示例 ${example}` }).click() + expect(await field.inputValue()).toBe(example) + expect(await dialog.getByRole('status').textContent()).toBe(hint) + } + await field.fill('/actual/plugin-directory') + expect(await dialog.getByRole('status').count()).toBe(0) + await dialog.getByRole('button', { name: '收起引导' }).click() + expect(await dialog.getByRole('note').textContent()).toContain('请确认插件来源可信') // A name the list already shows is refused without asking the Host. await field.fill('@fixture/bundle') await install.click() diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index c4a2f1d3b1..8ee3b11bcd 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: 555eccf8bfdd5029 - zh: e0886cbf6e62bfe3 + en: 3b18d6c7d6ae6a10 + zh: c14582da2fa4b0da /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: - en: 915fdd040b4d211c - zh: d77e0846f9cbc334 + en: 20e7310ab6867389 + zh: eed2957381abc0bd /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle: en: abc90531efa889d5 zh: 0562570f85e69c35 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index f11efd5b2a..b8ae9c6813 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -27,7 +27,7 @@ Use the **Plugins** entry in the Web sidebar to manage the profile's installed b ## 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. **Official** comes first and lists the bundles the installation ships for switching on — off until switched on, without an uninstall, and tagged **Experimental** for experimental features — followed by the official plugins that registered a configuration page; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Plugins** section. +Select **Plugins** in the sidebar to open **Plugin management**. The information button beside the subtitle explains that **Settings → Built-in plugins** shows the built-in inventory and runtime status. 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. **Official** comes first and lists the bundles the installation ships for switching on — off until switched on, without an uninstall, and tagged **Experimental** for experimental features — followed by the official plugins that registered a configuration page; **Installed** lists the bundles the profile holds. Cards are listed by name, so switching a bundle on or off does not move its card. A dependency without a bundle patch is not a plugin and is not listed unless the profile selects it, in which case it carries a problem tag. Global configuration remains in the Settings **Plugins** section. Installed bundles and their plugin rows display their own title and description in the current UI language on cards and detail pages. Each field falls back from exported locale `meta` to the accessible `package.json` at that plugin address; the final title is the full package or module name, with no package description when both sources omit it. See [Plugin display metadata](../../../docs/cookbook/adding-a-package.md#plugin-display-metadata) for the author format. Bundle cards, details, and component rows display the image declared by their own `package.json.icon`; absent or undecodable images retain the default artwork. Installation previews still use registry or manifest information. @@ -41,7 +41,7 @@ On first use, when pnpm uses the unconfigured official npm registry and npmmirro The Host sends GET requests to `https://registry.npmjs.org/-/ping` and `https://registry.npmmirror.com/-/ping` through its normal fetch proxy. A 2xx response wins; redirects and failures do not. The remaining request is cancelled, both response bodies are discarded, and cleanup completes before the result returns. The default deadline is 1500 ms and results, including unavailable results, are cached for five minutes. Configure `registryProbeTimeoutMs` and `registryProbeCacheTtlMs` on `ui-plugin-manager`; `registryProbeEnabled: false` disables probing. If both requests fail or time out, the existing default remains selected. No IP-location service or Session content is involved. -**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. +**Add plugin** takes a package name with an optional version, a Git address, a tarball, or an absolute local path; the dialog says a package name is what follows `dsh plugin add` in a README. Enter in the package or custom-registry field submits the install request, but Enter used to confirm an input-method candidate does not. **Install guide and examples** under the field opens a guide that shows the three common forms with an example each; **Use example** drops one into the field. Git and local-path examples require the actual address or path; a hint appears while either example remains in the field. A trust warning stays above **Install**, including when the guide is collapsed. **Registry** beside it names the registry the install asks first and unfolds the choice: pnpm's own registry, named by the registry it actually names — the official npm registry, the mainland China mirror, or its host — which keeps the neutral default name until the Host reads it, is listed once, and carries its host unless that host is already its name; each mirror the Host configured (`pluginManager.registries`), npmmirror as the mainland China mirror; and a typed http(s) address. The options use an opaque selector panel, fill rows only on hover, and show hosts in tertiary text without parentheses. The custom-address area highlights on hover; clicking anywhere in it selects the custom registry and focuses its input, whose focus style matches the package field. Keyboard focus alone preserves the selected registry; activating the custom radio or editing the address selects the custom registry. Tab moves from the selected radio to the address, and Shift+Tab moves back; leaving either end closes the selector and returns focus to **Registry**. Switching to an offered registry retains the custom URL draft for switching back. Submitting an invalid custom address blocks installation, opens the registry selector, and focuses the address. Invalid package and custom-address fields keep a red error border and add the shared blue inset focus ring on focus. The options float over the dialog from the control, so unfolding them never lengthens the card, and the card's content scrolls when the guide makes it taller than the viewport. The initial choice follows the response comparison above; the choice is then remembered in this browser (`localStorage`) and starts the next dialog, and a remembered address the Host no longer offers stays as a typed one and remains the requested address when installation starts before the registry list returns. **Install** first asks the Host to read what the spec names (`pluginManager.inspect`) at the chosen registry: a name the list already shows, a name no registry has, a path without a package, a package without a bundle patch, or a spec pnpm would refuse comes back under the field as one sentence, with the spec kept for editing; when no registry could be reached, the sentence names every one asked. The install then starts at the registry that answered. An accepted spec opens the installing screen, which shows the package's name, one-liner, and version as the Host read them and folds pnpm's command and output behind **Show install details**. A finished install offers **Enable now**, which switches the new bundle on, closes the dialog, and scrolls the list to it; closing instead leaves it installed and off. While the Host moves on to another registry, the installing screen says which registry could not serve the package and which is asked now, and each pnpm run behind the details carries a badge naming its registry. A failed install says what went wrong in one line, as the Host attributed it — no registry could be reached, naming every one asked; the host a GitHub address or a tarball link is fetched from could not be reached, which no registry helps; the package was not found, the disk is full, the profile is not writable, pnpm blocked a build script — with pnpm's output behind the details and **Retry** at hand, and **Change registry** beside it when the Host laid the failure at the registry it asked, which returns to the spec with the registry options open; the Host has already put the profile files back. When pnpm blocked a dependency's install scripts, the failed screen lists the packages whose scripts wait for permission and offers **Allow these scripts and retry** in place of **Retry**; the Host saves the permission in the profile's `pnpm-workspace.yaml`, which a failed run leaves as pnpm wrote it, then runs pnpm again, and the installed screen names what was allowed. A successful installation does not certify that a module can activate. During preparation and download, **Cancel install** asks the Host to stop the run and waits for confirmation. Loading the bundle cannot be cancelled. Closing with ×, Escape, or the backdrop hides the dialog immediately and requests cancellation when possible. **View installation** reopens the same task with its output; another installation cannot start while its outcome is pending or unconfirmed. Confirmed cancellation returns to the spec and shows a toast; the manifest and lockfile are restored, while downloaded files can remain. A lost installation response triggers a request to recover the result; **Check installation status** and reconnect retry that request. If the Host has no active request, **Installation result unavailable** allows returning to editing after checking the plugin list. A cancellation sent before acceptance waits and retries automatically; a failed cancellation can be retried manually. Hidden tasks report their outcome through toasts without reopening the dialog. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index c8a68347ea..a943102e3d 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -27,7 +27,7 @@ kind: "package-reference" ## 使用本包 -在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**官方**排在前面,列出安装随附、供开启的组合包——开启前保持关闭、没有卸载、属于实验性功能的带**实验性**标签——其后是注册了配置页的官方插件;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。 +在侧栏选择**插件**,打开**插件管理**。副标题旁的信息按钮说明,可在**设置 → 内置插件**查看内置插件清单及运行状态。页面首次打开时通过 `api-remotes` 读取清单与组合包;没有受管 profile 的 Host 上页面显示为不可用。**官方**排在前面,列出安装随附、供开启的组合包——开启前保持关闭、没有卸载、属于实验性功能的带**实验性**标签——其后是注册了配置页的官方插件;**已安装**列出 profile 持有的组合包。卡片按名称排序,启停组合包不会挪动它的卡片。没有组合包 patch 的依赖不是插件,除非 profile 选中了它才会带异常标签列出。全局配置仍在设置的**插件**分区中编辑。 已安装的组合包及其插件行在卡片和详情页中,按当前界面语言显示各自的标题与描述。每个字段先读取导出的 locale `meta`,缺失时回退到该插件地址下可访问的 `package.json`;标题最终使用完整包名或模块名,两处都没有描述时不提供包描述。作者格式见[插件展示元信息](../../../docs/cookbook/adding-a-package.zh.md#plugin-display-metadata)。组合包卡片、详情和组件行显示各自 `package.json.icon` 声明的图片;未声明或无法解码时保留默认插画。安装预览仍使用注册表或 manifest 信息。 @@ -41,7 +41,7 @@ kind: "package-reference" Host 通过普通 fetch 代理向 `https://registry.npmjs.org/-/ping` 和 `https://registry.npmmirror.com/-/ping` 发送 GET 请求,以最先返回 2xx 的源为结果;重定向和失败不参与选择。随后取消另一条请求、丢弃两个响应体,并在清理完成后返回。默认时限为 1500 毫秒,包括不可用结果在内的探测结果均缓存五分钟。在 `ui-plugin-manager` 上配置 `registryProbeTimeoutMs` 和 `registryProbeCacheTtlMs`;`registryProbeEnabled: false` 关闭探测。两条请求都失败或超时时保留现有默认源。不使用 IP 地区服务,也不发送 Session 内容。 -**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 +**添加插件**接受包名(可带版本)、Git 地址、压缩包或本地绝对路径;对话框说明包名就是 README 里 `dsh plugin add` 后面的那一段。在包名或自定义安装源输入框中按 Enter 会提交安装请求,但用于确认输入法候选词的 Enter 不会触发安装。输入框下方的**插件安装引导和示例**展开一段引导,给出三种常见形式各一个示例;**填入示例**把示例填进输入框。Git 地址和本地路径示例需要替换为实际值;输入框中保留示例时会显示提醒。可信来源提示始终显示在**安装**按钮上方,引导收起后也可见。旁边的**安装源**写着安装首先询问的注册表,展开后可选:pnpm 自身的注册表,按它实际指向的源命名——npm 官方源、中国大陆镜像源,其余以主机名显示——Host 读不到时保留中性的默认名称,只列一次,选项标题带上主机名(主机名即名称时不重复显示);Host 配置的每个镜像(`pluginManager.registries`),npmmirror 显示为中国大陆镜像源;以及手动输入的 http(s) 地址。选项使用不透明选择面板,仅悬停行使用底色,主机名不加括号并使用三级文字色。自定义地址区域在悬停时高亮;点击其中任意位置会选中自定义源并聚焦输入框,焦点样式与包名输入框一致。仅通过键盘聚焦不会改变已选安装源;激活自定义单选项或编辑地址才会选中自定义源。Tab 从已选单选项移到地址框,Shift+Tab 反向返回;从任一端移出时关闭选择器,并将焦点交还**安装源**按钮。切到预设源时保留自定义地址草稿,切回后继续使用。提交无效的自定义地址时会阻止安装,展开安装源选择器并聚焦地址框。包名和自定义地址输入框无效时保留红色错误边框,聚焦时加上共享的蓝色内嵌焦点环。选项从控件上浮在对话框之上,展开不会拉长卡片;引导使卡片高过视口时,卡片内容可以滚动。初始选择遵循上文的响应比较规则;之后选择记在本浏览器(`localStorage`)里,下次打开对话框从它开始,Host 不再提供的已记住地址保留为手动输入的地址;在安装源列表返回前开始安装,也仍请求该地址。**安装**先让 Host 在所选注册表读出 spec 指向什么(`pluginManager.inspect`):列表中已有的名字、所有注册表都没有的名字、没有包的路径、没有组合包 patch 的包,或 pnpm 会拒绝的 spec,都以一句话回到输入框下方,spec 保留可继续编辑;所有注册表都连不上时,这句话列出问过的每一个。随后安装从作答的那个注册表开始。通过检查的 spec 打开安装中界面,展示 Host 读到的包名、一句话简介和版本,pnpm 的命令与输出折叠在**查看安装详情**之后。安装完成后提供**立即启用**:启用新组合包、关闭对话框并把列表滚动到它;直接关闭则让它保持已安装但关闭。Host 改问另一个注册表时,安装中界面会说明哪个源没能提供这个包、现在改问哪个,详情里每次 pnpm 运行都带一个写明所用源的标记。安装失败时按 Host 的归因用一行话说明原因——所有安装源都连不上,并列出问过的每一个;GitHub 地址或压缩包链接自身的主机连不上,换源无济于事;包不存在、磁盘已满、profile 不可写、pnpm 拦下了构建脚本——pnpm 输出在详情里,**重试**就在手边,Host 把失败归于所问注册表时还在旁边提供**更换安装源**,回到 spec 输入界面并展开安装源选项;Host 已经把 profile 文件放回原样。pnpm 拦下依赖的安装脚本时,失败界面列出等待允许的包,并以**允许这些脚本并重试**取代**重试**;Host 把授权写进 profile 的 `pnpm-workspace.yaml`(失败的运行保留 pnpm 写入的这个文件)再运行 pnpm,安装完成界面会说明允许了哪些脚本。安装成功不代表模块一定能够激活。 准备和下载期间,**取消安装**会请求 Host 停止运行并等待确认。加载组合包的阶段不可取消。点击 ×、按 Escape 或点击遮罩会立即隐藏对话框,并在可以取消时请求取消。**查看安装任务**会重新打开同一任务并保留输出;结果待定或尚未确认时不能发起另一项安装。确认取消后回到 spec 输入界面并显示 toast;manifest 与 lockfile 已恢复,已下载文件可能保留。安装响应丢失后会请求恢复结果;**核对安装状态**和重连会重试该请求。Host 已无活动请求时,**未能获取安装结果**允许检查插件列表后返回编辑。早于接收确认的取消请求会等待并自动重试;取消失败可手动重试。隐藏的任务通过 toast 通知结果,不会重新弹出对话框。 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 90e7684f37..896e99e130 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css @@ -47,12 +47,42 @@ } .pageIntro { + display: flex; + align-items: center; + gap: 4px; margin: 4px 0 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); } +.infoButton { + flex: none; + width: 24px; + height: 24px; + padding: 0; + color: var(--dsw-alias-label-tertiary); +} + +.infoPanel { + position: fixed; + z-index: 1100; + box-sizing: border-box; + width: min(300px, calc(100vw - 24px)); + padding: 8px; + border: 0; + box-shadow: var(--dsw-elevation-panel); + outline: none; +} + +.infoContent { + margin: 0; + padding: 4px 6px; + color: var(--dsw-alias-label-secondary); + font-size: 13px; + line-height: 20px; +} + .toolbar { display: flex; align-items: center; @@ -82,16 +112,6 @@ gap: 6px; } -.banner { - margin: 0; - padding: 8px 12px; - border-radius: var(--dsw-radius-md); - font-size: 12px; - line-height: 18px; - background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent); - color: var(--dsw-alias-label-primary); -} - .group { display: flex; flex-direction: column; @@ -474,6 +494,21 @@ min-width: 0; } +.installFooter { + display: flex; + flex: 1; + flex-direction: column; + gap: 12px; + min-width: 0; +} + +.templateHint { + margin: -6px 0 0; + font-size: 12px; + line-height: 18px; + color: var(--dsw-alias-label-secondary); +} + /* Where pnpm installs: the profile directory the first run names. */ .installLocation { margin: 0; @@ -561,26 +596,27 @@ margin: 0; font-size: 12px; line-height: 18px; - color: var(--dsw-alias-label-tertiary); + color: var(--dsw-alias-label-secondary); } -/* The security warning that closes the guide: plugins run with the person's own permissions. */ -.guideSafety { +/* Trust guidance stays beside the install action, including while examples are collapsed. */ +.installSafety { display: flex; align-items: flex-start; gap: 8px; margin: 0; padding: 10px 12px; border-radius: var(--dsw-radius-md); - background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent); + background: var(--dsw-alias-state-warn-tertiary); font-size: 12px; line-height: 18px; - color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)); + color: var(--dsw-alias-label-secondary); } -.guideSafety > svg { +.installSafety > svg { flex: none; margin-top: 2px; + color: var(--dsw-alias-state-warn-primary); } .guideList { @@ -641,7 +677,7 @@ } .guideExample code { - font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); + font-family: var(--ds-font-family-code); } .guideExampleLabel { @@ -1020,7 +1056,7 @@ label.registryOption { } .resultWarn { - background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent); + background: var(--dsw-alias-state-warn-tertiary); } /* The approval a blocked run asks for: the packages whose install scripts pnpm did not run, and the one action. */ @@ -1029,9 +1065,9 @@ label.registryOption { flex-direction: column; gap: 8px; padding: 12px 14px; - border: 0.5px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 40%, transparent); + border: 0.5px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent); border-radius: var(--dsw-radius-lg); - background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 8%, transparent); + background: var(--dsw-alias-state-warn-tertiary); } .approvalTitle { @@ -1052,8 +1088,8 @@ label.registryOption { margin: 0; font-size: 12.5px; line-height: 18px; - font-weight: 600; - color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)); + font-weight: 500; + color: var(--dsw-alias-label-secondary); } .approvalList { diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index ced0d9c348..f8e31f5ab6 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -13,10 +13,10 @@ import { useEffect, useId, useRef, useState, type ReactNode } from 'react' import { createPortal } from 'react-dom' import type { PluginInstallFailureKind, Registry } from '@deepseek-ai/dsh-api-remotes/client' import { - Button, IconCheckCircleFillRegular, IconChevronDownOutlineRegular, IconChevronLeftOutlineMedium, + Button, HoverCard, IconCheckCircleFillRegular, IconChevronDownOutlineRegular, IconChevronLeftOutlineMedium, IconChevronRightOutlineRegular, IconCloseOutlineMedium, - IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular, - IconWarningOutlineRegular, Input, Modal, pointerModality, + IconInfoOutlineRegular, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular, + IconWarningOutlineRegular, Input, MenuSurface, Modal, pointerModality, PluginArtworkDefault, PluginArtworkLoop, PluginArtworkSearch, PluginArtworkSubagent, PluginArtworkTerminal, StateDot, Switch, Tag, TerminalBlock, Toast, Tooltip, useAnchoredPosition, useDismissOnOutsidePointer, type IconProps, type StateDotState, type TerminalBlockLabels, @@ -50,6 +50,42 @@ export type PluginManagerPageProps = type RenderConfig = PluginManagerPageProps['renderSlot'] type ResolveText = PluginManagerFace['resolveText'] +/** Explain where the built-in inventory lives without leaving the management page. */ +function PluginInventoryInfo({ t }: { readonly t: Translate }): ReactNode { + const [open, setOpen] = useState(false) + const trigger = useRef(null) + const panel = useRef(null) + const id = useId() + const position = useAnchoredPosition({ open, anchorRef: trigger, panelRef: panel, gap: 8, margin: 12 }) + const ready = open && position !== null + useDismissOnOutsidePointer(trigger, open, setOpen, panel) + useEffect(() => { + if (ready) panel.current?.focus() + }, [ready]) + const content =

{t('infoDescription')}

+ return <> + { setOpen(value => !value) }}> +