feat(web): add read-only Loader plugin inventory

This commit is contained in:
ZiyaZhang
2026-08-11 21:10:14 -07:00
parent 258331f0da
commit 6ed5df964e
64 changed files with 1429 additions and 19 deletions
+2 -2
View File
@@ -56,9 +56,9 @@ describe('web e2e: plugin configuration section', () => {
await page.getByRole('button', { name: '设置', exact: true }).click()
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByRole('button', { name: '插件' }).click()
await dialog.getByRole('button', { name: '插件配置', exact: true }).click()
await expect
.poll(() => dialog.getByRole('button', { name: '插件' }).getAttribute('aria-current'), { timeout: 5_000 })
.poll(() => dialog.getByRole('button', { name: '插件配置', exact: true }).getAttribute('aria-current'), { timeout: 5_000 })
.toBe('true')
return dialog
}
+25 -1
View File
@@ -23,6 +23,8 @@ import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/settings-chrome', import.meta.url))
const DIALOG_EXPECTED = join(SNAPSHOT_DIR, 'dialog.expected.md')
const PLUGINS_EXPECTED = join(SNAPSHOT_DIR, 'plugins.expected.md')
const PLUGIN_ROW_SELECTOR = '[data-plugin-entry$="ui-settings"]'
const MODE = webSnapshotMode()
describe('web e2e: settings modal and General preferences', () => {
@@ -92,6 +94,28 @@ describe('web e2e: settings modal and General preferences', () => {
await dialog.getByRole('button', { name: '模型' }).click()
await expect.poll(() => dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current'), { timeout: 5_000 }).toBe('true')
expect(await dialog.getByRole('button', { name: '通用设置' }).getAttribute('aria-current')).toBeNull()
// Plugins is a read-only projection of the same assembled Loader tree.
// Capture one stable shipped row rather than the whole inventory so adding
// an unrelated plugin does not rewrite this surface's golden.
await dialog.getByRole('button', { name: '插件', exact: true }).click()
await dialog.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 10_000 })
const pluginRow = dialog.locator(PLUGIN_ROW_SELECTOR)
await pluginRow.waitFor({ timeout: 10_000 })
const expectedPluginCount = [...scaffold.ctx.loader.entries()]
.filter(entry => !entry.options.group)
.length
expect(await dialog.getByRole('searchbox', { name: '搜索插件' }).count()).toBe(1)
expect(await dialog.locator('[data-plugin-entry]').count()).toBe(expectedPluginCount)
expect(await dialog.locator('[data-plugin-count]').getAttribute('data-plugin-count'))
.toBe(String(expectedPluginCount))
expect(await dialog.getByRole('button', { name: '插件', exact: true }).getAttribute('aria-current')).toBe('true')
expect(await dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current')).toBeNull()
const pluginsSnapshot = await captureStableAria(
page,
PLUGIN_ROW_SELECTOR,
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(PLUGINS_EXPECTED, pluginsSnapshot, MODE)
// Close path 1: Escape.
await page.keyboard.press('Escape')
await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0)
@@ -454,6 +478,6 @@ describe('web e2e: settings modal and General preferences', () => {
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['dialog.expected.md'])
await assertFixtureInventory(SNAPSHOT_DIR, ['dialog.expected.md', 'plugins.expected.md'])
})
})
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -7,6 +7,9 @@
- button "模型":
- img
- text: 模型
- button "插件":
- img
- text: 插件
- button "Agent 预设":
- img
- text: Agent 预设
@@ -0,0 +1,4 @@
- listitem "ui-settings, 存活, 已启用":
- strong: ui-settings
- img "存活"
- text: 已启用
+2 -2
View File
@@ -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 docs/config-catalog.md
config-catalog.md: 2bb1f315e02c3f4bab379593227f1c381818a9fa
config-catalog.zh.md: 4be51942de65a7e8afdaf71a44573ba1cdd6230d
config-catalog.md: 83e0d3e3a8bd1b5f0b5d70e1552e0fd9b70eccf1
config-catalog.zh.md: 21cf904708c5eae1c71b2c85843eb80926a4912e
+2
View File
@@ -2770,6 +2770,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-permission` ([`packages/client/ui-permission/src/index.ts`](../packages/client/ui-permission/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plugin-config` ([`packages/client/ui-plugin-config/src/index.ts`](../packages/client/ui-plugin-config/src/index.ts))
- `@deepseek-ai/dsh-client-ui-plugins` ([`packages/client/ui-plugins/src/index.ts`](../packages/client/ui-plugins/src/index.ts))
- `@deepseek-ai/dsh-client-ui-question` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
@@ -2792,6 +2793,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-goal-session` — requires `agents` · `goals` · `sessions` ([`packages/goal/goal-session/src/index.ts`](../packages/goal/goal-session/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker-auto` — requires `httpServer` · `loader` ([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker-native` ([`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts))
- `@deepseek-ai/dsh-host-plugin-inventory` — requires `loader` ([`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts))
- `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts))
- `@deepseek-ai/dsh-lsp` ([`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts))
- `@deepseek-ai/dsh-pty` ([`packages/pty/pty/src/index.ts`](../packages/pty/pty/src/index.ts))
+2
View File
@@ -2771,6 +2771,7 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-permission`[`packages/client/ui-permission/src/index.ts`](../packages/client/ui-permission/src/index.ts)
- `@deepseek-ai/dsh-client-ui-plan`[`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts)
- `@deepseek-ai/dsh-client-ui-plugin-config`[`packages/client/ui-plugin-config/src/index.ts`](../packages/client/ui-plugin-config/src/index.ts)
- `@deepseek-ai/dsh-client-ui-plugins`[`packages/client/ui-plugins/src/index.ts`](../packages/client/ui-plugins/src/index.ts)
- `@deepseek-ai/dsh-client-ui-question`[`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings`[`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)
- `@deepseek-ai/dsh-client-ui-settings-general`[`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)
@@ -2793,6 +2794,7 @@ export interface Config {
- `@deepseek-ai/dsh-goal-session` — 需要 `agents` · `goals` · `sessions`[`packages/goal/goal-session/src/index.ts`](../packages/goal/goal-session/src/index.ts)
- `@deepseek-ai/dsh-host-directory-picker-auto` — 需要 `httpServer` · `loader`[`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts)
- `@deepseek-ai/dsh-host-directory-picker-native`[`packages/host/directory-picker-native/src/index.ts`](../packages/host/directory-picker-native/src/index.ts)
- `@deepseek-ai/dsh-host-plugin-inventory` — 需要 `loader`[`packages/host/plugin-inventory/src/index.ts`](../packages/host/plugin-inventory/src/index.ts)
- `@deepseek-ai/dsh-llm`[`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)
- `@deepseek-ai/dsh-lsp`[`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts)
- `@deepseek-ai/dsh-pty`[`packages/pty/pty/src/index.ts`](../packages/pty/pty/src/index.ts)
+2 -2
View File
@@ -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 docs/module-graph.md
module-graph.md: e24d2601fd74f58eec8c5e5b1a35701e1800baaa
module-graph.zh.md: b71da431b05634c4c599096c8cd33fed23a6be45
module-graph.md: 9ca575137f3ebdddbc77e89c3b3b97da90c94843
module-graph.zh.md: 6978ff30b7e8e927a0d0a10dd7b06cfbbeb1f7e9
+16 -1
View File
@@ -162,6 +162,7 @@ flowchart TD
pkg_client_ui_permission["client-ui-permission"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_plugin_config["client-ui-plugin-config"]
pkg_client_ui_plugins["client-ui-plugins"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_question["client-ui-question"]
pkg_client_ui_settings["client-ui-settings"]
@@ -219,6 +220,7 @@ flowchart TD
pkg_host_directory_picker_auto["host-directory-picker-auto"]
pkg_host_directory_picker_browse["host-directory-picker-browse"]
pkg_host_directory_picker_native["host-directory-picker-native"]
pkg_host_plugin_inventory["host-plugin-inventory"]
pkg_host_webserver["host-webserver"]
end
subgraph group_interaction["packages/interaction"]
@@ -358,6 +360,9 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
pkg_host_plugin_inventory --> pkg_brand
pkg_host_plugin_inventory --> pkg_invariants
pkg_host_plugin_inventory --> pkg_type_meta
pkg_user_id --> pkg_brand
pkg_user_id --> pkg_invariants
pkg_user_id --> pkg_paths
@@ -642,6 +647,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
pkg_api_remotes --> pkg_llm
pkg_api_remotes --> pkg_session
@@ -1136,6 +1142,13 @@ flowchart TD
pkg_client_ui_plugin_config --> pkg_client_ui_slots
pkg_client_ui_plugin_config --> pkg_client_web_react
pkg_client_ui_plugin_config --> pkg_invariants
pkg_client_ui_plugins --> pkg_api_remotes
pkg_client_ui_plugins --> pkg_client_locale
pkg_client_ui_plugins --> pkg_client_runtime
pkg_client_ui_plugins --> pkg_client_ui_primitives
pkg_client_ui_plugins --> pkg_client_ui_settings
pkg_client_ui_plugins --> pkg_client_ui_slots
pkg_client_ui_plugins --> pkg_invariants
pkg_client_ui_question --> pkg_api_remotes
pkg_client_ui_question --> pkg_client_locale
pkg_client_ui_question --> pkg_invariants
@@ -1374,6 +1387,7 @@ flowchart TD
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta) |
| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
@@ -1443,7 +1457,7 @@ flowchart TD
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
@@ -1522,6 +1536,7 @@ flowchart TD
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-plugin-config`](../packages/client/ui-plugin-config) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-plugins`](../packages/client/ui-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+16 -1
View File
@@ -164,6 +164,7 @@ flowchart TD
pkg_client_ui_permission["client-ui-permission"]
pkg_client_ui_plan["client-ui-plan"]
pkg_client_ui_plugin_config["client-ui-plugin-config"]
pkg_client_ui_plugins["client-ui-plugins"]
pkg_client_ui_primitives["client-ui-primitives"]
pkg_client_ui_question["client-ui-question"]
pkg_client_ui_settings["client-ui-settings"]
@@ -221,6 +222,7 @@ flowchart TD
pkg_host_directory_picker_auto["host-directory-picker-auto"]
pkg_host_directory_picker_browse["host-directory-picker-browse"]
pkg_host_directory_picker_native["host-directory-picker-native"]
pkg_host_plugin_inventory["host-plugin-inventory"]
pkg_host_webserver["host-webserver"]
end
subgraph group_interaction["packages/interaction"]
@@ -360,6 +362,9 @@ flowchart TD
pkg_subprocess_e2b --> pkg_timeout
pkg_frontend_static --> pkg_host_webserver
pkg_frontend_static --> pkg_invariants
pkg_host_plugin_inventory --> pkg_brand
pkg_host_plugin_inventory --> pkg_invariants
pkg_host_plugin_inventory --> pkg_type_meta
pkg_user_id --> pkg_brand
pkg_user_id --> pkg_invariants
pkg_user_id --> pkg_paths
@@ -644,6 +649,7 @@ flowchart TD
pkg_api_remotes --> pkg_commands
pkg_api_remotes --> pkg_credentials
pkg_api_remotes --> pkg_goal
pkg_api_remotes --> pkg_host_plugin_inventory
pkg_api_remotes --> pkg_invariants
pkg_api_remotes --> pkg_llm
pkg_api_remotes --> pkg_session
@@ -1138,6 +1144,13 @@ flowchart TD
pkg_client_ui_plugin_config --> pkg_client_ui_slots
pkg_client_ui_plugin_config --> pkg_client_web_react
pkg_client_ui_plugin_config --> pkg_invariants
pkg_client_ui_plugins --> pkg_api_remotes
pkg_client_ui_plugins --> pkg_client_locale
pkg_client_ui_plugins --> pkg_client_runtime
pkg_client_ui_plugins --> pkg_client_ui_primitives
pkg_client_ui_plugins --> pkg_client_ui_settings
pkg_client_ui_plugins --> pkg_client_ui_slots
pkg_client_ui_plugins --> pkg_invariants
pkg_client_ui_question --> pkg_api_remotes
pkg_client_ui_question --> pkg_client_locale
pkg_client_ui_question --> pkg_invariants
@@ -1376,6 +1389,7 @@ flowchart TD
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta) |
| [`user-id`](../packages/session/user-id) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`storage-domain`](../packages/storage/storage-domain) | `storage` | [`invariants`](../packages/support/invariants), [`storage`](../packages/storage/storage) |
@@ -1445,7 +1459,7 @@ flowchart TD
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) |
@@ -1524,6 +1538,7 @@ flowchart TD
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`client-ui-plugin-config`](../packages/client/ui-plugin-config) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-plugins`](../packages/client/ui-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-question`](../packages/client/ui-question) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) |
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
+2 -2
View File
@@ -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/api/remotes/README.md
README.md: cc903af7204ca715c6c7931cfe44823d4d5fc71e
README.zh.md: fe34b8774c9864cef442ff8a58f22f541d40768a
README.md: 288c63c9f43654dfec428a6a8955dc537efe81a6
README.zh.md: 1fd599b08ecc1b4ae1dba738ce8946aa4eab5946
+1 -1
View File
@@ -6,7 +6,7 @@ Two-sided BFF for Host Remote capabilities selected by this application. The Hos
`createApiRemoteAgentResolver()` reuses live Agents, resumes ordinary cold sessions, deduplicates concurrent resumes, preserves the subagent ownership fence, and configures the same resolver for TypeRT `agent` and `session` lookups. The standard Web API Proxy supplies its Agent defaults and scope setup, then uses the returned resolver for legacy methods, so migrated and unmigrated methods share one policy implementation.
The current Client assembly mounts only the Goal Remote contribution. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, and cancellation. The Client entry consumes the shared `TypeRTClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
The current Client assembly mounts the Goal Remote contribution and the read-only Host plugin inventory contribution (`pluginInventory/list`). Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, and cancellation. The Client entry consumes the shared `TypeRTClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
This package contains no transport or Host service discovery logic. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
+1 -2
View File
@@ -6,8 +6,7 @@
`createApiRemoteAgentResolver()` 会复用 live Agent、恢复普通冷会话、对并发恢复去重、保留 subagent ownership fence,并为 TypeRT `agent``session` lookup 配置同一个 resolver。标准 Web API Proxy 提供 Agent 默认值和 scope 设置,再将返回的 resolver 用于旧方法,使已迁移与未迁移方法共用同一份策略实现。
当前 Client 组合挂载 Goal Remote 贡献。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用与取消。Client 入口通过 Cordis 消费共享的 `TypeRTClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
当前 Client 组合挂载 Goal Remote 贡献和只读 Host 插件清单贡献(`pluginInventory/list`。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用与取消。Client 入口通过 Cordis 消费共享的 `TypeRTClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
本包不包含传输逻辑或 Host 服务发现逻辑。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
+2
View File
@@ -64,6 +64,7 @@
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
@@ -78,6 +79,7 @@
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-credentials": "workspace:^",
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-agent-presets": "workspace:^",
+3 -1
View File
@@ -3,11 +3,13 @@
import type { Context } from '@deepseek-ai/cordis'
import commandsRemote from '@deepseek-ai/dsh-commands/remote'
import goalsRemote from '@deepseek-ai/dsh-goal/remote'
import pluginInventoryRemote from '@deepseek-ai/dsh-host-plugin-inventory/remote'
import type { TypeRTClientRemote } from '@deepseek-ai/dsh-type-meta'
export type { TypeRTClientRemote as ClientRemote } from '@deepseek-ai/dsh-type-meta'
export type {} from '@deepseek-ai/dsh-commands/remote'
export type {} from '@deepseek-ai/dsh-goal/remote'
export type {} from '@deepseek-ai/dsh-host-plugin-inventory/remote'
// The forwarded-event allowlist's selection seat: without it in the consumer's
// compilation face `TypeRTRemoteEvent` is `never` and every `$on` call fails.
export type { ApiRemoteForwardedEvent } from '../types.ts'
@@ -54,7 +56,7 @@ export const inject = ['remote']
export async function apply(ctx: Context): Promise<() => Promise<void>> {
const disposers: Array<() => Promise<void>> = []
try {
for (const contribution of [commandsRemote, goalsRemote]) {
for (const contribution of [commandsRemote, goalsRemote, pluginInventoryRemote]) {
disposers.push(await ctx.remote.$mount(contribution))
}
} catch (error) {
@@ -26,6 +26,9 @@
{
"path": "../../goal/goal"
},
{
"path": "../../host/plugin-inventory"
},
{
"path": "../../interaction/commands"
},
+7
View File
@@ -80,6 +80,10 @@
- id: directory-picker
name: '@deepseek-ai/dsh-host-directory-picker-auto'
# Read-only projection of current Loader entries for trusted client RPCs.
- id: plugin-inventory
name: '@deepseek-ai/dsh-host-plugin-inventory'
# The API gateway: the transport-agnostic dispatch face every client shape
# shares. The base layer's agent-default-model service owns the default model.
- id: api-gateway
@@ -172,6 +176,9 @@
- id: ui-models
name: '@deepseek-ai/dsh-client-ui-models'
- id: ui-plugins
name: '@deepseek-ai/dsh-client-ui-plugins'
- id: ui-conversation
name: '@deepseek-ai/dsh-client-ui-conversation'
+2
View File
@@ -62,6 +62,7 @@
"@deepseek-ai/dsh-client-ui-layout": "workspace:^",
"@deepseek-ai/dsh-client-ui-model": "workspace:^",
"@deepseek-ai/dsh-client-ui-models": "workspace:^",
"@deepseek-ai/dsh-client-ui-plugins": "workspace:^",
"@deepseek-ai/dsh-client-ui-permission": "workspace:^",
"@deepseek-ai/dsh-client-ui-plan": "workspace:^",
"@deepseek-ai/dsh-client-ui-plugin-config": "workspace:^",
@@ -86,6 +87,7 @@
"@deepseek-ai/dsh-host-directory-picker-auto": "workspace:^",
"@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
"@deepseek-ai/dsh-host-plugin-inventory": "workspace:^",
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@deepseek-ai/dsh-message-feedback": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
+2 -2
View File
@@ -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/README.md
README.md: 75abe408952ed66dcc237ce489e417f61159bcc3
README.zh.md: 5432efcb0a5ebc410093da4c3ec6c2e07c4520ca
README.md: 236531281c17ef982982e97caad99491584bd0b5
README.zh.md: e619ffaa6341f509537342bde90344141d4c8f64
+1
View File
@@ -41,6 +41,7 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
| [`ui-settings/`](ui-settings/README.md) | Hosts the settings interface and its extension areas. |
| [`ui-settings-general/`](ui-settings-general/README.md) | Provides the general settings section. |
| [`ui-models/`](ui-models/README.md) | Provides model-provider configuration and DeepSeek onboarding. |
| [`ui-plugins/`](ui-plugins/README.md) | Shows the current Host Loader entries in a read-only Settings section. |
Each child reference owns its contract and detailed behavior. The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) and [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) own the cross-package composition and loading decisions.
+1
View File
@@ -41,6 +41,7 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
| [`ui-settings/`](ui-settings/README.md) | 承载设置界面及其扩展区域。 |
| [`ui-settings-general/`](ui-settings-general/README.md) | 提供常规设置分区。 |
| [`ui-models/`](ui-models/README.md) | 提供模型提供方配置与 DeepSeek 配置引导。 |
| [`ui-plugins/`](ui-plugins/README.md) | 在只读设置分区中展示当前 Host Loader 条目。 |
每个子文档负责自身的约定和详细行为。[slot 系统标准](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md)与 [Web 客户端架构 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)负责跨包组合与加载决策。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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-plugins/README.md
README.md: c3236935b2a568c6996fcac8469f061a813cd95c
README.zh.md: 77810bccf95a55761518f89909fddd99818f686a
+20
View File
@@ -0,0 +1,20 @@
# @deepseek-ai/dsh-client-ui-plugins
English | [中文](README.zh.md)
Read-only Plugins section for Web Settings. The browser plugin registers one localized `settings.section` contribution with id `plugin-inventory`, after Models, and lets the Settings shell supply its ordinary fallback icon. It performs no Remote read during plugin activation; mounting the section lazily calls `ctx.remote.pluginInventory.list()` through [`api-remotes`](../../api/remotes/README.md).
The page renders a searchable two-column catalog of compact cards. Each card uses the local Loader id as its title, a colored root-Fiber status dot, and a small effective-enablement tag. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details. The registration uses `ctx.slots.inject()`, so it follows late Settings declaration, redeclaration, locale changes, and teardown without owning another global store.
## Model Experience
None, as this package only visualizes a Host-owned deployment snapshot in browser Settings and registers nothing model-facing.
#### KV Cache effect
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **One snapshot per mount or retry** — the page does not subscribe to Loader changes or automatically refetch after reconnect; reopening the section obtains a new snapshot.
- **Read-only Loader view** — local search does not add provenance, current-browser activation diagnosis, grouping by source, or plugin mutation controls.
+20
View File
@@ -0,0 +1,20 @@
# @deepseek-ai/dsh-client-ui-plugins
[English](README.md) | 中文
Web 设置中的只读“插件”分区。浏览器插件在“模型”之后注册一个 id 为 `plugin-inventory` 的本地化 `settings.section` 贡献,并由 Settings shell 提供常规的回退图标。插件激活期间不会读取 Remote;挂载该分区时,组件才通过 [`api-remotes`](../../api/remotes/README.md) 懒调用 `ctx.remote.pluginInventory.list()`
页面以可搜索的双列紧凑卡片展示清单。每张卡片使用 Loader 本地 id 作为标题,以彩色圆点表示根 Fiber 状态,以小标签表示有效启停状态。加载、空结果、无匹配结果与通用失败状态只属于已挂载组件;读取失败后可以重试,且不会暴露传输细节。注册使用 `ctx.slots.inject()`,因此能跟随 Settings 的延迟声明、重新声明、本地化变化与 teardown,而不拥有另一份全局 store。
## 模型体验
无,因为本包只在浏览器设置中展示 Host 拥有的部署快照,不注册任何模型接口。
#### KV Cache 影响
无;本包既不组装也不发送提供方请求。
## 已知限制与暂缓事项
- **每次挂载或重试只读取一份快照** —— 页面不订阅 Loader 变化,也不会在重连后自动重新读取;重新打开分区会取得新快照。
- **只读 Loader 视图** —— 本地搜索不会额外引入来源、按来源分组、当前浏览器激活诊断或插件修改控件。
+80
View File
@@ -0,0 +1,80 @@
{
"name": "@deepseek-ai/dsh-client-ui-plugins",
"description": "Read-only Cordis Loader plugin inventory in Web settings",
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/client/ui-plugins"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dsh": {
"client": {
"inject": [
"@deepseek-ai/dsh-api-remotes",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-locale"
],
"platform": "web"
}
},
"scripts": {
"bundle": "tsdown",
"watch": "tsdown --watch"
},
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^",
"react": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-api-remotes": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/types/**/*.d.ts"
]
}
@@ -0,0 +1,213 @@
.section {
display: flex;
flex-direction: column;
gap: 14px;
width: 100%;
max-width: 760px;
color: var(--dsw-alias-label-primary);
}
.heading h2,
.catalogHeading h3,
.status,
.failure p {
margin: 0;
}
.heading h2 {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.status,
.failure {
font-size: 13px;
line-height: 20px;
color: var(--dsw-alias-label-tertiary);
}
.failure {
display: flex;
align-items: center;
gap: 10px;
color: var(--dsw-alias-state-error-primary);
}
.failure button {
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
padding: 4px 10px;
background: transparent;
color: var(--dsw-alias-label-primary);
font: inherit;
cursor: pointer;
}
.catalog {
display: flex;
flex-direction: column;
gap: 12px;
}
.search {
position: relative;
display: flex;
align-items: center;
width: 100%;
color: var(--dsw-alias-label-tertiary);
}
.search > svg {
position: absolute;
left: 12px;
pointer-events: none;
}
.search input {
width: 100%;
height: 36px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 8px;
padding: 0 34px 0 36px;
outline: none;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-primary);
font: inherit;
font-size: 13px;
}
.search input::placeholder {
color: var(--dsw-alias-label-tertiary);
}
.search input:focus-visible {
border-color: var(--dsw-alias-state-business-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
}
.catalogHeading {
display: flex;
align-items: baseline;
gap: 7px;
padding: 0 2px;
}
.catalogHeading h3 {
font-size: 13px;
line-height: 20px;
font-weight: 600;
}
.catalogHeading span {
font-size: 12px;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
font-variant-numeric: tabular-nums;
}
.cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-items: start;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.card {
min-width: 0;
overflow: hidden;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 10px;
background: var(--dsw-alias-bg-layer-3);
}
.cardContent {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
min-height: 52px;
border: 0;
padding: 12px 14px;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
}
.cardTitle {
min-width: 0;
overflow: hidden;
font-size: 14px;
line-height: 20px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.cardTrailing {
display: inline-flex;
flex: none;
align-items: center;
gap: 7px;
color: var(--dsw-alias-label-tertiary);
}
.statusDot {
display: inline-block;
width: 7px;
height: 7px;
flex: none;
border-radius: 999px;
background: var(--dsw-alias-label-tertiary);
}
.statusDot[data-phase='active'] {
background: var(--dsw-alias-state-success-primary);
}
.statusDot[data-phase='failed'] {
background: var(--dsw-alias-state-error-primary);
}
.statusDot[data-phase='loading'] {
background: var(--dsw-alias-state-business-primary);
}
.configTag {
display: inline-flex;
align-items: center;
min-height: 20px;
border-radius: 5px;
padding: 1px 6px;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-secondary);
font-size: 11px;
line-height: 16px;
white-space: nowrap;
}
.configTag[data-enabled='true'] {
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);
color: var(--dsw-alias-state-success-primary);
}
.visuallyHidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
@media (max-width: 680px) {
.cards {
grid-template-columns: minmax(0, 1fr);
}
}
@@ -0,0 +1,149 @@
import { useEffect, useId, useMemo, useState, type ReactNode } from 'react'
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
import { IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { PluginsKey } from './locales.ts'
import css from './PluginSettingsSection.module.css'
/** Registration-side Remote face used by the section. */
export interface PluginSettingsSectionInjected {
/** Read a current Host inventory snapshot. */
list: ClientRemote['pluginInventory']['list']
}
type PluginInventorySnapshot = Awaited<ReturnType<PluginSettingsSectionInjected['list']>>
type PluginInventoryEntry = PluginInventorySnapshot['entries'][number]
type PluginFiberPhase = PluginInventoryEntry['fiberPhase']
/** Full component props assembled by the Settings slot renderer. */
export type PluginSettingsSectionProps =
PropsRuntime<'settings.section'>
& PropsLocale<'settings.plugins'>
& InjectFace<PluginSettingsSectionInjected>
type ViewState =
| { readonly status: 'loading' }
| { readonly status: 'error' }
| { readonly status: 'ready'; readonly snapshot: PluginInventorySnapshot }
const PHASE_KEYS = {
pending: 'pending',
loading: 'loadingPhase',
active: 'active',
failed: 'failed',
unloading: 'unloading',
} satisfies Record<Exclude<PluginFiberPhase, null>, PluginsKey>
/** Localized accessible label for one root Fiber phase. */
function phaseLabel(
phase: PluginFiberPhase,
t: PluginSettingsSectionProps['t'],
): string {
return phase === null ? t('unobserved') : t(PHASE_KEYS[phase])
}
/** Whether an inventory row matches the local catalog query. */
function matches(entry: PluginInventoryEntry, normalizedQuery: string): boolean {
if (normalizedQuery.length === 0) return true
return [entry.displayId, entry.entryId]
.some(value => value.toLocaleLowerCase().includes(normalizedQuery))
}
/** Render the read-only current Loader inventory. */
export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps): ReactNode {
const titleId = useId()
const [request, setRequest] = useState(0)
const [query, setQuery] = useState('')
const [state, setState] = useState<ViewState>({ status: 'loading' })
useEffect(() => {
let current = true
void Promise.resolve().then(() => list()).then(
(snapshot) => { if (current) setState({ status: 'ready', snapshot }) },
() => { if (current) setState({ status: 'error' }) },
)
return () => { current = false }
}, [list, request])
const normalizedQuery = query.trim().toLocaleLowerCase()
const filteredEntries = useMemo(
() => state.status === 'ready'
? state.snapshot.entries.filter(entry => matches(entry, normalizedQuery))
: [],
[normalizedQuery, state],
)
const retry = (): void => {
setState({ status: 'loading' })
setRequest(value => value + 1)
}
return (
<section className={css.section} aria-labelledby={titleId} aria-busy={state.status === 'loading'}>
<header className={css.heading}>
<h2 id={titleId}>{t('title')}</h2>
</header>
{state.status === 'loading' ? <p className={css.status}>{t('loading')}</p> : null}
{state.status === 'error' ? (
<div className={css.failure}>
<p role="alert">{t('error')}</p>
<button type="button" onClick={retry}>{t('retry')}</button>
</div>
) : null}
{state.status === 'ready' ? (
<div className={css.catalog}>
<label className={css.search}>
<IconSearchOutline16 aria-hidden="true" />
<span className={css.visuallyHidden}>{t('search')}</span>
<input
type="search"
value={query}
placeholder={t('search')}
aria-label={t('search')}
onChange={event => setQuery(event.currentTarget.value)}
/>
</label>
<div className={css.catalogHeading}>
<h3>{t('catalog')}</h3>
<span data-plugin-count={filteredEntries.length}>{filteredEntries.length}</span>
</div>
{state.snapshot.entries.length === 0 ? <p className={css.status}>{t('empty')}</p> : null}
{state.snapshot.entries.length > 0 && filteredEntries.length === 0
? <p className={css.status}>{t('emptySearch')}</p>
: null}
{filteredEntries.length > 0 ? (
<ul className={css.cards}>
{filteredEntries.map((entry) => {
const status = phaseLabel(entry.fiberPhase, t)
return (
<li
className={css.card}
key={entry.entryId}
data-plugin-entry={entry.entryId}
aria-label={`${entry.displayId}, ${status}, ${t(entry.enabled ? 'enabledTag' : 'disabledTag')}`}
>
<div className={css.cardContent}>
<strong className={css.cardTitle}>{entry.displayId}</strong>
<span className={css.cardTrailing}>
<span
className={css.statusDot}
data-phase={entry.fiberPhase ?? 'unobserved'}
role="img"
aria-label={status}
title={status}
/>
<span className={css.configTag} data-enabled={entry.enabled ? 'true' : 'false'}>
{t(entry.enabled ? 'enabledTag' : 'disabledTag')}
</span>
</span>
</div>
</li>
)
})}
</ul>
) : null}
</div>
) : null}
</section>
)
}
@@ -0,0 +1,42 @@
/** Read-only Host plugin inventory registered into Web Settings. */
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
import type {} from '@deepseek-ai/dsh-client-locale/client'
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import { PluginSettingsSection, type PluginSettingsSectionInjected } from './PluginSettingsSection.tsx'
import { en, zh, type PluginsKey } from './locales.ts'
export type { PluginSettingsSectionInjected, PluginSettingsSectionProps } from './PluginSettingsSection.tsx'
export type { PluginsKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Read-only Host plugin inventory copy. */
'settings.plugins': PluginsKey
}
}
/** Dictionary namespace owned by this plugin. */
export const NS = 'settings.plugins'
/** Services required by the Settings registration and generated Remote face. */
export const inject = ['slots', 'locale', 'remote', 'remote.pluginInventory']
/** Register the lazy plugin inventory page below Models in Settings. */
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-plugins: dictionaries')
const t = ctx.locale.bind(NS)
const list: ClientRemote['pluginInventory']['list'] = () => ctx.remote.pluginInventory.list()
const injected = (): PluginSettingsSectionInjected => ({ list })
ctx.slots.inject('settings.section', () => ctx.slots.register({
name: 'settings.section',
id: 'plugin-inventory',
order: 15,
label: () => t('nav'),
locale: NS,
inject: injected,
}, PluginSettingsSection))
}
@@ -0,0 +1,46 @@
/** Copy dictionaries for the plugin inventory Settings section. */
/** Simplified Chinese dictionary and key source of truth. */
export const zh = {
nav: '插件',
title: '插件',
loading: '正在读取插件…',
error: '暂时无法读取插件。',
retry: '重试',
search: '搜索插件',
catalog: '插件列表',
empty: '暂无插件。',
emptySearch: '没有匹配的插件。',
enabledTag: '已启用',
disabledTag: '已停用',
unobserved: '无根 Fiber',
pending: '等待依赖',
loadingPhase: '加载中',
active: '存活',
failed: '失败',
unloading: '卸载中',
} satisfies Record<string, string>
/** Plugin inventory locale key union. */
export type PluginsKey = keyof typeof zh
/** English dictionary checked against the Chinese key set. */
export const en = {
nav: 'Plugins',
title: 'Plugins',
loading: 'Reading plugins…',
error: 'Plugins are temporarily unavailable.',
retry: 'Retry',
search: 'Search plugins',
catalog: 'Plugin list',
empty: 'No plugins are available.',
emptySearch: 'No matching plugins.',
enabledTag: 'Enabled',
disabledTag: 'Disabled',
unobserved: 'No root Fiber',
pending: 'Pending',
loadingPhase: 'Loading',
active: 'Active',
failed: 'Failed',
unloading: 'Unloading',
} satisfies Record<PluginsKey, string>
+6
View File
@@ -0,0 +1,6 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.css'
+4
View File
@@ -0,0 +1,4 @@
/** Host loader entry for the browser implementation exported from `./client`. */
/** Host plugin body — no host-side behavior for the plugin settings section. */
export function apply(): void {}
@@ -0,0 +1,20 @@
/** Package-owned invariant companion. @module @deepseek-ai/dsh-client-ui-plugins/invariant */
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-plugins'
/** Cordis companion plugin name. */
export const name = 'client-ui-plugins-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: this package owns a read-only Settings contribution. */
const install: InvariantInstaller = () => {}
/** Register this package's invariant companion. */
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,87 @@
// @vitest-environment jsdom
import { Context, Service } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { apply, inject, NS } from '../src/client/index.ts'
import { PluginSettingsSection } from '../src/client/PluginSettingsSection.tsx'
import type { PluginSettingsSectionInjected } from '../src/client/PluginSettingsSection.tsx'
usePinnedBrowserLanguages('zh-CN')
afterEach(cleanup)
const EMPTY = { entries: [] }
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
ctx.provide('locale', locale)
class RemoteService extends Service {
constructor(serviceCtx: Context) {
super(serviceCtx, 'remote')
}
}
new RemoteService(ctx)
const list = vi.fn(() => Promise.resolve(EMPTY))
ctx.provide('remote.pluginInventory', { list })
return { ctx, slots: ctx.get('slots') as SlotsService, locale, list }
}
function declare(slots: SlotsService): () => void {
return slots.register({
name: 'root',
children: { 'settings.section': { kind: 'list', scope: 'root' } },
} as never, () => null)
}
describe('ui-plugins browser plugin', () => {
it('declares only the services used by the Settings Remote contribution', () => {
expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.pluginInventory'])
})
it('registers a localized section without reading the Remote eagerly', async () => {
const b = await bench()
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const entry = b.slots.entries('settings.section')[0]!
expect(entry.component).toBe(PluginSettingsSection)
expect(entry.options).toMatchObject({ id: 'plugin-inventory', order: 15 })
expect(entry.locale).toBe(NS)
expect(resolveSlotLabel(entry.options.label)).toBe('插件')
expect(b.list).not.toHaveBeenCalled()
const injected = (entry.inject as unknown as () => PluginSettingsSectionInjected)()
await expect(injected.list()).resolves.toEqual(EMPTY)
expect(b.list).toHaveBeenCalledOnce()
await b.ctx.fiber.dispose()
})
it('follows locale and recovers across late declaration and declarer reload', async () => {
const b = await bench()
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(b.slots.entries('settings.section')).toHaveLength(0)
const stop = declare(b.slots)
await vi.waitFor(() => { expect(b.slots.entries('settings.section')).toHaveLength(1) })
b.locale.setLocale('en')
expect(resolveSlotLabel(b.slots.entries('settings.section')[0]!.options.label)).toBe('Plugins')
stop()
expect(b.slots.entries('settings.section')).toHaveLength(0)
declare(b.slots)
await vi.waitFor(() => {
expect(b.slots.entries('settings.section')[0]?.component).toBe(PluginSettingsSection)
})
await fiber.dispose()
expect(b.slots.entries('settings.section')).toHaveLength(0)
expect(() => b.locale.register(NS, 'zh', {})).not.toThrow()
await b.ctx.fiber.dispose()
})
})
@@ -0,0 +1,100 @@
// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PluginSettingsSection } from '../src/client/PluginSettingsSection.tsx'
import type {
PluginSettingsSectionInjected,
PluginSettingsSectionProps,
} from '../src/client/PluginSettingsSection.tsx'
import { en, type PluginsKey } from '../src/client/locales.ts'
afterEach(cleanup)
type Snapshot = Awaited<ReturnType<PluginSettingsSectionInjected['list']>>
const t = ((key: PluginsKey): string => en[key]) as PluginSettingsSectionProps['t']
const unusedHook = (() => { throw new Error('unused by plugin inventory') }) as never
function props(list: PluginSettingsSectionInjected['list']): PluginSettingsSectionProps {
return {
close: vi.fn(),
useSessions: unusedHook,
useWorkspaces: unusedHook,
t,
list,
}
}
const SNAPSHOT = {
entries: [
{ entryId: 'active', displayId: 'active-name', enabled: true, fiberPhase: 'active' },
{ entryId: 'pending', displayId: 'pending-name', enabled: true, fiberPhase: 'pending' },
{ entryId: 'loading', displayId: 'loading-name', enabled: true, fiberPhase: 'loading' },
{ entryId: 'failed', displayId: 'failed-name', enabled: true, fiberPhase: 'failed' },
{ entryId: 'unloading', displayId: 'unloading-name', enabled: true, fiberPhase: 'unloading' },
{ entryId: 'disabled-entry', displayId: 'disabled-name', enabled: false, fiberPhase: null },
],
} as unknown as Snapshot
describe('PluginSettingsSection', () => {
it('renders searchable two-column-card semantics with dots and tags', async () => {
const deferred = Promise.withResolvers<Snapshot>()
const list = vi.fn(() => deferred.promise)
const view = render(<PluginSettingsSection {...props(list)} />)
expect(screen.getByText(en.loading)).toBeTruthy()
await act(async () => { deferred.resolve(SNAPSHOT) })
expect(list).toHaveBeenCalledOnce()
expect(screen.getByRole('searchbox', { name: en.search })).toBeTruthy()
expect(screen.getByRole('heading', { name: en.catalog })).toBeTruthy()
expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('6')
expect(screen.getAllByRole('listitem')).toHaveLength(6)
expect(screen.getAllByText(en.enabledTag)).toHaveLength(5)
expect(screen.getByText(en.disabledTag)).toBeTruthy()
for (const value of ['Active', 'Pending', 'Loading', 'Failed', 'Unloading', 'No root Fiber']) {
expect(screen.getByRole('img', { name: value })).toBeTruthy()
}
expect(screen.getByRole('listitem', { name: 'active-name, Active, Enabled' })).toBeTruthy()
})
it('filters by local id or Loader entry id', async () => {
render(<PluginSettingsSection {...props(async () => SNAPSHOT)} />)
const search = await screen.findByRole('searchbox', { name: en.search })
fireEvent.change(search, { target: { value: 'disabled-entry' } })
expect(screen.getAllByRole('listitem')).toHaveLength(1)
expect(screen.getByText('disabled-name')).toBeTruthy()
fireEvent.change(search, { target: { value: 'pending' } })
expect(screen.getAllByRole('listitem')).toHaveLength(1)
expect(screen.getByText('pending-name')).toBeTruthy()
fireEvent.change(search, { target: { value: 'not-a-plugin' } })
expect(screen.queryAllByRole('listitem')).toHaveLength(0)
expect(screen.getByText(en.emptySearch)).toBeTruthy()
})
it('shows a generic failure and retries into the empty state', async () => {
const list = vi.fn<PluginSettingsSectionInjected['list']>()
.mockRejectedValueOnce(new Error('private transport detail'))
.mockResolvedValueOnce({ entries: [] })
render(<PluginSettingsSection {...props(list)} />)
expect((await screen.findByRole('alert')).textContent).toBe(en.error)
expect(screen.queryByText('private transport detail')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: en.retry }))
await waitFor(() => { expect(list).toHaveBeenCalledTimes(2) })
expect(await screen.findByText(en.empty)).toBeTruthy()
})
it('contains a synchronous Remote failure and ignores a result after unmount', async () => {
const syncFailure = vi.fn(() => { throw new Error('namespace unavailable') }) as PluginSettingsSectionInjected['list']
const failed = render(<PluginSettingsSection {...props(syncFailure)} />)
expect((await screen.findByRole('alert')).textContent).toBe(en.error)
failed.unmount()
const deferred = Promise.withResolvers<Snapshot>()
const pending = render(<PluginSettingsSection {...props(() => deferred.promise)} />)
pending.unmount()
await act(async () => { deferred.resolve(SNAPSHOT) })
})
})
@@ -0,0 +1,15 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as PluginsInvariant from '../src/invariant.ts'
describe('ui-plugins invariant companion', () => {
it('registers the empty installer and keeps the node half inert', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(PluginsInvariant).await()).resolves.toBeDefined()
const { apply } = await import('../src/index.ts')
apply()
await ctx.fiber.dispose()
})
})
+36
View File
@@ -0,0 +1,36 @@
{
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../api/remotes/tsconfig.client.json"
},
{
"path": "../locale"
},
{
"path": "../runtime"
},
{
"path": "../ui-settings"
},
{
"path": "../ui-primitives"
},
{
"path": "../ui-slots"
},
{
"path": "../../support/invariants"
}
]
}
@@ -0,0 +1,3 @@
import { clientBundle } from '../tsdown.client.ts'
export default clientBundle('@deepseek-ai/dsh-client-ui-plugins', ['lib/types/index.js', 'lib/types/invariant.js'])
+2 -2
View File
@@ -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/host/README.md
README.md: 926cb0b6b87a8ee76cb2dab745a31f620f4e7f5c
README.zh.md: 7ef057ee56e56ddc2baa7092ccbe44fb161b7448
README.md: 1c3b6ab3192fe35a5532183414e45d1b02325e57
README.zh.md: a062d5fce055e3266953993d532a86bec1375377
+1
View File
@@ -13,6 +13,7 @@ The host side of the dsh web GUI: the API gateway every client shape shares, and
| [`directory-picker-native/`](directory-picker-native/README.md) | Native directory-picker backend and browser interaction | registers `ctx.directoryPicker` |
| [`directory-picker-browse/`](directory-picker-browse/README.md) | In-app directory-browser backend and interaction | registers `ctx.directoryPicker` |
| [`directory-picker-auto/`](directory-picker-auto/README.md) | Host-adaptive picker composition | mounts a backend |
| [`plugin-inventory/`](plugin-inventory/README.md) | Read-only projection of current Loader entries | Remote `pluginInventory/list` |
`apiproxy` remains transport-independent; [`client/connection`](../client/connection/README.md) supplies the browser/HTTP carrier. Picker implementations replace one another behind the shared seam.
+1
View File
@@ -13,6 +13,7 @@ dsh Web GUI 的宿主侧:所有客户端形态共享的 API 网关,以及承
| [`directory-picker-native/`](directory-picker-native/README.md) | 原生目录选择器后端和浏览器交互 | 注册 `ctx.directoryPicker` |
| [`directory-picker-browse/`](directory-picker-browse/README.md) | 应用内目录浏览器后端和交互 | 注册 `ctx.directoryPicker` |
| [`directory-picker-auto/`](directory-picker-auto/README.md) | 宿主自适应选择器组合 | 挂载一个后端 |
| [`plugin-inventory/`](plugin-inventory/README.md) | 当前 Loader 条目的只读投影 | Remote `pluginInventory/list` |
`apiproxy` 保持传输无关;[`client/connection`](../client/connection/README.md) 提供浏览器/HTTP 载体。选择器实现可在共享 seam 后互相替换。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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/host/plugin-inventory/README.md
README.md: d7a50824d337c66a30ba4332ffb1e36f9ad46547
README.zh.md: e424b968f1f692fb30b3b0e49efe6c3cd5fadf70
+22
View File
@@ -0,0 +1,22 @@
# @deepseek-ai/dsh-host-plugin-inventory
English | [中文](README.zh.md)
Read-only Host projection of the current Cordis Loader tree. `PluginInventoryService` registers the `pluginInventory` service and publishes one generated direct Remote, `pluginInventory/list`. Every call reads `ctx.loader.entries()` directly, skips structural group rows, and returns the remaining entries in Loader order with only their Loader entry id, local display id, effective enablement, and current root Fiber phase.
The phase is `pending`, `loading`, `active`, `failed`, or `unloading`; it is `null` when the entry has no live root Fiber. The snapshot is intentionally point-in-time: Loader remains the sole lifecycle authority, while this package owns no cache, history, provenance model, event stream, or mutation path. Its public payload types live under `./types`, and TypeRT generates the Host and Client Remote artifacts exposed by `./typert` and `./remote`.
The service is Remote-only and deliberately declares no same-process Cordis `Context` merge. Client packages consume it through the explicit [`api-remotes`](../../api/remotes/README.md) assembly rather than importing the Host implementation.
## Model Experience
None, as this Host-only inventory projection registers no prompt, tool, message, or provider request.
#### KV Cache effect
None; this package never assembles model input.
## Known Limitations and Deferred Work
- **Point-in-time state only** — the result contains no durable failure history or subscription; a missing root Fiber is reported as `null`, regardless of why no live root exists.
- **No provenance or mutation** — the service does not identify which bundle, profile, or override introduced an entry, and it cannot enable, disable, add, or remove plugins.
@@ -0,0 +1,22 @@
# @deepseek-ai/dsh-host-plugin-inventory
[English](README.md) | 中文
当前 Cordis Loader 树的只读 Host 投影。`PluginInventoryService` 注册 `pluginInventory` 服务,并发布一个由 TypeRT 生成的直接 Remote`pluginInventory/list`。每次调用都直接读取 `ctx.loader.entries()`,跳过结构性的 group 行,再按 Loader 顺序返回其余条目,并且只包含 Loader 条目 id、本地展示 id、有效启用状态与当前根 Fiber 阶段。
阶段为 `pending``loading``active``failed``unloading`;条目没有存活的根 Fiber 时则为 `null`。该快照刻意只表示调用当下:Loader 仍是唯一的生命周期权威,本包不拥有缓存、历史、来源模型、事件流或修改路径。公开 payload 类型位于 `./types`TypeRT 生成由 `./typert``./remote` 导出的 Host 和 Client Remote 产物。
该服务仅供 Remote 使用,刻意不声明同进程 Cordis `Context` merge。Client 包通过显式的 [`api-remotes`](../../api/remotes/README.md) 组合消费它,而不导入 Host 实现。
## 模型体验
无,因为这个仅限 Host 的清单投影不注册提示词、工具、消息或提供方请求。
#### KV Cache 影响
无;本包从不组装模型输入。
## 已知限制与暂缓事项
- **仅表示调用当下** —— 结果不包含持久的失败历史或订阅;只要不存在存活的根 Fiber,就会报告 `null`,而不区分其原因。
- **无来源与修改能力** —— 服务不识别条目由哪个 bundle、profile 或 override 引入,也不能启用、停用、添加或移除插件。
@@ -0,0 +1,70 @@
{
"name": "@deepseek-ai/dsh-host-plugin-inventory",
"description": "Read-only Remote projection of current Cordis Loader plugin state",
"version": "0.0.1-rc.1",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/host/plugin-inventory"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./types": {
"types": "./lib/types/types.d.ts",
"default": "./lib/types/types.js"
},
"./typert": {
"types": "./lib/typert.host.d.ts",
"default": "./lib/typert.host.js"
},
"./remote": {
"types": "./lib/typert.remote-client.d.ts",
"default": "./lib/typert.remote-client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.js",
"lib/types/**/*.d.ts",
"lib/typert.host.js",
"lib/typert.host.d.ts",
"lib/typert.remote-client.js",
"lib/typert.remote-client.d.ts",
"lib/typert.remote-client.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"dependencies": {
"zod": "^4.4.3"
},
"peerDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-type-meta": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}
@@ -0,0 +1,72 @@
/** Read-only projection of the current Cordis Loader plugin entries. */
import type { Context, FiberState } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import { GatewayService, Remote } from '@deepseek-ai/dsh-type-meta'
// TypeRT-generated ./typert and ./remote artifacts import Zod at runtime.
import type {} from 'zod'
import type {
PluginEntryId,
PluginFiberPhase,
PluginInventoryEntry,
PluginInventorySnapshot,
} from './types.ts'
export type * from './types.ts'
/** Brand an existing Loader-tree entry id at the owning boundary. */
function pluginEntryId(value: string): PluginEntryId {
return value as PluginEntryId
}
/** Runtime mirror: FiberState is a cross-package const enum. */
const FIBER_STATE = {
PENDING: 0 as FiberState.PENDING,
LOADING: 1 as FiberState.LOADING,
ACTIVE: 2 as FiberState.ACTIVE,
FAILED: 3 as FiberState.FAILED,
DISPOSED: 4 as FiberState.DISPOSED,
UNLOADING: 5 as FiberState.UNLOADING,
} as const
/** Complete public projection of Cordis Fiber states. */
const FIBER_PHASE = {
[FIBER_STATE.PENDING]: 'pending',
[FIBER_STATE.LOADING]: 'loading',
[FIBER_STATE.ACTIVE]: 'active',
[FIBER_STATE.FAILED]: 'failed',
[FIBER_STATE.DISPOSED]: null,
[FIBER_STATE.UNLOADING]: 'unloading',
} as const satisfies Record<FiberState, PluginFiberPhase>
/** Remote-only service exposing the Loader's current non-group entry state. */
export class PluginInventoryService extends GatewayService {
static inject = ['loader']
constructor(ctx: Context) {
super(ctx, 'pluginInventory')
}
/**
* Read the Loader directly on every call. Cordis's internal plugin/status
* events already maintain Entry.fiber and Fiber.state, so a second cache
* would only add another lifecycle truth to keep synchronized.
* @returns Current non-group Loader entries in Loader order.
*/
@Remote('list')
list(): PluginInventorySnapshot {
const entries: PluginInventoryEntry[] = []
for (const entry of this.ctx.loader.entries()) {
if (entry.options.group) continue
entries.push({
entryId: pluginEntryId(entry.id),
displayId: entry.options.id,
enabled: !entry.disabled,
fiberPhase: entry.fiber === undefined ? null : FIBER_PHASE[entry.fiber.state],
})
}
return { entries }
}
}
export default PluginInventoryService
@@ -0,0 +1,20 @@
/** Package-owned invariant companion. @module @deepseek-ai/dsh-host-plugin-inventory/invariant */
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-host-plugin-inventory'
/** Cordis companion plugin name. */
export const name = 'host-plugin-inventory-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: every snapshot is projected directly from Loader-owned state. */
const install: InvariantInstaller = () => {}
/** Register this package's invariant companion. */
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -0,0 +1,28 @@
import type { Branded } from '@deepseek-ai/dsh-brand'
/** Stable Loader-tree identity of one configured plugin entry. */
export type PluginEntryId = Branded<'PluginEntryId'>
/** Lifecycle state of an entry's root Fiber, or null when it has no live root Fiber. */
export type PluginFiberPhase =
| 'pending'
| 'loading'
| 'active'
| 'failed'
| 'unloading'
| null
/** One non-group Loader entry exposed to trusted clients. */
export interface PluginInventoryEntry {
readonly entryId: PluginEntryId
/** Local Loader id used as the compact card title. */
readonly displayId: string
/** Effective Loader enablement, including disabled ancestor groups. */
readonly enabled: boolean
readonly fiberPhase: PluginFiberPhase
}
/** Point-in-time inventory returned by the plugin inventory Remote. */
export interface PluginInventorySnapshot {
readonly entries: readonly PluginInventoryEntry[]
}
@@ -0,0 +1,16 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as PluginInventoryInvariant from '../src/invariant.ts'
describe('plugin-inventory invariant companion', () => {
it('registers the package-owned empty installer', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
const fiber = ctx.plugin(PluginInventoryInvariant)
await expect(fiber.await()).resolves.toBeDefined()
await fiber.dispose()
await expect(ctx.plugin(PluginInventoryInvariant).await()).resolves.toBeDefined()
await ctx.fiber.dispose()
})
})
@@ -0,0 +1,89 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context, type Plugin } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { remoteMethods } from '@deepseek-ai/dsh-type-meta'
import PluginInventoryService from '../src/index.ts'
const contexts: Context[] = []
afterEach(async () => {
await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose()))
})
const activePlugin: Plugin.Function = () => {}
const pendingPlugin: Plugin.Object = {
inject: ['neverReady'],
apply() {},
}
async function harness(): Promise<{
ctx: Context
inventory: PluginInventoryService
}> {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(Loader)
ctx.loader.builtins.active = activePlugin
ctx.loader.builtins.pending = pendingPlugin
await ctx.plugin(PluginInventoryService)
const inventory = ctx.get('pluginInventory') as PluginInventoryService
return { ctx, inventory }
}
describe('PluginInventoryService', () => {
it('publishes one direct list method under the pluginInventory namespace', async () => {
const { inventory } = await harness()
expect(inventory.typertGateway).toMatchObject({
serviceKey: 'pluginInventory',
namespace: 'pluginInventory',
})
expect(remoteMethods(inventory)).toEqual([
{ method: 'list', invocation: { kind: 'direct' } },
])
})
it('projects current non-group Loader entries without a second cache', async () => {
const { ctx, inventory } = await harness()
const activeId = await ctx.loader.create({ name: 'cordis:active' })
const pendingId = await ctx.loader.create({ name: 'cordis:pending' })
const disabledId = await ctx.loader.create({
name: 'cordis:not-installed',
disabled: true,
})
await ctx.loader.create({ name: 'cordis:active', group: true })
expect(inventory.list()).toEqual({
entries: [
{
entryId: activeId,
displayId: activeId,
enabled: true,
fiberPhase: 'active',
},
{
entryId: pendingId,
displayId: pendingId,
enabled: true,
fiberPhase: 'pending',
},
{
entryId: disabledId,
displayId: disabledId,
enabled: false,
fiberPhase: null,
},
],
})
await ctx.loader.update(activeId, { disabled: true })
expect(inventory.list().entries.find(entry => entry.entryId === activeId)).toEqual({
entryId: activeId,
displayId: activeId,
enabled: false,
fiberPhase: null,
})
await ctx.loader.remove(pendingId)
expect(inventory.list().entries.some(entry => entry.entryId === pendingId)).toBe(false)
})
})
@@ -0,0 +1,27 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/loader"
},
{
"path": "../../util/brand"
},
{
"path": "../../typert/type-meta"
},
{
"path": "../../support/invariants"
}
]
}
+73
View File
@@ -846,6 +846,9 @@ importers:
'@deepseek-ai/dsh-goal':
specifier: workspace:^
version: link:../../goal/goal
'@deepseek-ai/dsh-host-plugin-inventory':
specifier: workspace:^
version: link:../../host/plugin-inventory
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
@@ -1593,6 +1596,9 @@ importers:
'@deepseek-ai/dsh-client-ui-plugin-config':
specifier: workspace:^
version: link:../../client/ui-plugin-config
'@deepseek-ai/dsh-client-ui-plugins':
specifier: workspace:^
version: link:../../client/ui-plugins
'@deepseek-ai/dsh-client-ui-question':
specifier: workspace:^
version: link:../../client/ui-question
@@ -1656,6 +1662,9 @@ importers:
'@deepseek-ai/dsh-host-directory-picker-native':
specifier: workspace:^
version: link:../../host/directory-picker-native
'@deepseek-ai/dsh-host-plugin-inventory':
specifier: workspace:^
version: link:../../host/plugin-inventory
'@deepseek-ai/dsh-host-webserver':
specifier: workspace:^
version: link:../../host/webserver
@@ -2582,6 +2591,48 @@ importers:
specifier: ^18.2.0
version: 18.3.1
packages/client/ui-plugins:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-api-remotes':
specifier: workspace:^
version: link:../../api/remotes
'@deepseek-ai/dsh-client-locale':
specifier: workspace:^
version: link:../locale
'@deepseek-ai/dsh-client-runtime':
specifier: workspace:^
version: link:../runtime
'@deepseek-ai/dsh-client-test-runtime':
specifier: workspace:^
version: link:../test-runtime
'@deepseek-ai/dsh-client-ui-primitives':
specifier: workspace:^
version: link:../ui-primitives
'@deepseek-ai/dsh-client-ui-settings':
specifier: workspace:^
version: link:../ui-settings
'@deepseek-ai/dsh-client-ui-slots':
specifier: workspace:^
version: link:../ui-slots
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
'@testing-library/react':
specifier: ^16.1.0
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/react':
specifier: ~18.3.1
version: 18.3.31
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
packages/client/ui-primitives:
dependencies:
'@shikijs/langs':
@@ -4942,6 +4993,28 @@ importers:
specifier: workspace:^
version: link:../../support/invariants
packages/host/plugin-inventory:
dependencies:
zod:
specifier: ^4.4.3
version: 4.4.3
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/cordis-plugin-loader':
specifier: workspace:^
version: link:../../../vendor/loader
'@deepseek-ai/dsh-brand':
specifier: workspace:^
version: link:../../util/brand
'@deepseek-ai/dsh-invariants':
specifier: workspace:^
version: link:../../support/invariants
'@deepseek-ai/dsh-type-meta':
specifier: workspace:^
version: link:../../typert/type-meta
packages/host/webserver:
dependencies:
'@deepseek-ai/schemastery':
@@ -91,6 +91,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/ui-plugins': { kind: 'none', reason: 'Browser-side inventory projection; registers nothing model-facing.' },
'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' },
@@ -105,6 +106,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/host/directory-picker-native': { kind: 'none', reason: 'The GUI-host picking backend registers nothing model-facing.' },
'packages/host/webserver': { kind: 'none', reason: 'The HTTP carrier bridges browser and API handler and registers nothing model-facing.' },
'packages/host/frontend-static': { kind: 'none', reason: 'The SPA dist server answers browser asset requests and registers nothing model-facing.' },
'packages/host/plugin-inventory': { kind: 'none', reason: 'Host-side read-only Loader projection; registers nothing model-facing.' },
'packages/bundle/base': { kind: 'indirect', reason: 'The bundle is a patch-list carrier; each inserted row\'s package owns its model-facing behavior.' },
'packages/bundle/headless': { kind: 'none', reason: 'The one-shot runner submits the task as an ordinary user message; prompts and tools belong to the composed base and headless bundles.' },
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
+3
View File
@@ -161,6 +161,8 @@
"@deepseek-ai/dsh-host-apiproxy/client": ["./packages/host/apiproxy/src/fetch/client.ts"],
"@deepseek-ai/dsh-host-apiproxy/*": ["./packages/host/apiproxy/src/*"],
"@deepseek-ai/dsh-host-webserver": ["./packages/host/webserver/src"],
"@deepseek-ai/dsh-host-plugin-inventory": ["./packages/host/plugin-inventory/src"],
"@deepseek-ai/dsh-host-plugin-inventory/types": ["./packages/host/plugin-inventory/src/types.ts"],
"@deepseek-ai/dsh-client-ui-slots": ["./packages/client/ui-slots/src"],
"@deepseek-ai/dsh-client-ui-attachment": ["./packages/client/ui-attachment/src"],
"@deepseek-ai/dsh-client-ui-primitives": ["./packages/client/ui-primitives/src"],
@@ -199,6 +201,7 @@
"@deepseek-ai/dsh-client-ui-settings": ["./packages/client/ui-settings/src"],
"@deepseek-ai/dsh-client-ui-settings-general": ["./packages/client/ui-settings-general/src"],
"@deepseek-ai/dsh-client-ui-models": ["./packages/client/ui-models/src"],
"@deepseek-ai/dsh-client-ui-plugins": ["./packages/client/ui-plugins/src"],
"@deepseek-ai/dsh-client-locale": ["./packages/client/locale/src"],
"@deepseek-ai/dsh-client-web": ["./packages/client/web/src"],
// sdk/ folders are role-named without their npm-side sdk/jsonrpc prefixes,
+1
View File
@@ -81,6 +81,7 @@
{ "path": "./packages/client/ui-settings" },
{ "path": "./packages/client/ui-settings-general" },
{ "path": "./packages/client/ui-models" },
{ "path": "./packages/client/ui-plugins" },
{ "path": "./packages/client/locale" },
{ "path": "./packages/client/web" },
{ "path": "./apps/web" }
+1
View File
@@ -281,6 +281,7 @@
{ "path": "./packages/host/directory-picker-browse" },
{ "path": "./packages/host/directory-picker-native" },
{ "path": "./packages/host/frontend-static" },
{ "path": "./packages/host/plugin-inventory" },
{ "path": "./packages/host/webserver" },
{ "path": "./packages/sdk/client" },
{ "path": "./packages/sdk/protocol" },