From 59ecca49fb91f7b430a842613cc5e549be747c72 Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 14 Aug 2026 03:26:25 +0800 Subject: [PATCH 1/3] feat: built-in community plugins, profile switches, and working skin center Ship dsh-genui, dsh-annotation, and the dsh-web-ui aggregate inside the Web profile with per-product Settings switches persisted to the profile patch, plus LINUX DO and plugin credits. - Web profile template adds the three community bundles; the web-app bundle declares the aggregate's nine entry packages and the whale-song skin as direct pinned dependencies so the profile module fallback can resolve them - Loopback-only host plugin-control gateway plus the browser Plugin switches tab (restart-time settings) - Desktop declares the plugin-control peer, stages the skin-center skins tree through extraResources, and the postinstall links the workspace copy for source launches - Patch @linxin666/dsh-client-ui-skin-center via patchedDependencies: resolve the skins tree beside an ancestor node_modules, always insert the active skin row (the published aggregate wires none), and reconcile the live Loader tree after apply so packaged Electron, which cannot provide Cordis HMR, applies skins instantly - Bilingual credits for LINUX DO and the three plugin projects; third-party notices regenerated --- .../2026-08-11-plugin-settings-tabs.i18n.yaml | 4 +- .../2026-08-11-plugin-settings-tabs.md | 6 +- .../2026-08-11-plugin-settings-tabs.zh.md | 6 +- ...n-community-plugins-and-controls.i18n.yaml | 6 + ...built-in-community-plugins-and-controls.md | 51 + ...lt-in-community-plugins-and-controls.zh.md | 51 + .gitattributes | 5 + README.i18n.yaml | 4 +- README.md | 7 + README.zh.md | 7 + THIRD_PARTY_NOTICES.md | 14 + apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 4 +- apps/cli/README.zh.md | 4 +- apps/cli/src/profile-boot.ts | 3 + apps/desktop/electron-builder.yml | 6 + apps/desktop/package.json | 1 + apps/desktop/scripts/prepare-package.mjs | 50 +- apps/web/tests/scaffold.ts | 36 +- apps/web/tests/settings-chrome.e2e.ts | 62 +- .../plugin-config/section.expected.md | 3 + .../plugin-controls.expected.md | 22 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 31 + docs/config-catalog.zh.md | 31 + knip.json | 15 +- package.json | 2 +- packages/api/remotes/README.i18n.yaml | 4 +- packages/api/remotes/README.md | 2 +- packages/api/remotes/README.zh.md | 2 +- packages/api/remotes/package.json | 2 + packages/api/remotes/src/client/index.ts | 7 + packages/api/remotes/tsconfig.client.json | 3 + packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/boot/app-boot/src/index.ts | 2 + packages/boot/app-boot/src/profile.ts | 9 +- packages/boot/app-boot/tests/profile.spec.ts | 31 + packages/bundle/web-app/README.i18n.yaml | 4 +- packages/bundle/web-app/README.md | 19 + packages/bundle/web-app/README.zh.md | 19 + packages/bundle/web-app/cordis.patch.yml | 33 + packages/bundle/web-app/package.json | 49 +- .../README.i18n.yaml | 6 + .../ui-settings-plugin-control/README.md | 31 + .../ui-settings-plugin-control/README.zh.md | 31 + .../ui-settings-plugin-control/package.json | 82 ++ .../PluginControlSettingsTab.module.css | 225 ++++ .../src/client/PluginControlSettingsTab.tsx | 159 +++ .../src/client/index.ts | 58 ++ .../src/client/locales.ts | 50 + .../src/client/protocol.ts | 40 + .../src/css-modules.d.ts | 6 + .../ui-settings-plugin-control/src/index.ts | 4 + .../src/invariant.ts | 20 + .../tests/browser-plugin.client.spec.tsx | 116 +++ .../tests/components.client.spec.tsx | 130 +++ .../tests/protocol.client.spec.ts | 29 + .../ui-settings-plugin-control/tsconfig.json | 39 + .../tsdown.config.ts | 3 + .../src/client/PluginsSettingsSection.tsx | 2 +- .../src/client/slot-catalog.ts | 1 + packages/host/plugin-control/README.i18n.yaml | 6 + packages/host/plugin-control/README.md | 32 + packages/host/plugin-control/README.zh.md | 32 + packages/host/plugin-control/package.json | 60 ++ .../host/plugin-control/src/control-file.ts | 88 ++ packages/host/plugin-control/src/index.ts | 245 +++++ packages/host/plugin-control/src/invariant.ts | 20 + packages/host/plugin-control/src/types.ts | 30 + .../host/plugin-control/tests/control.spec.ts | 285 ++++++ packages/host/plugin-control/tsconfig.json | 33 + ...linxin666__dsh-client-ui-skin-center.patch | 148 +++ pnpm-lock.yaml | 961 ++++++++++++++++-- pnpm-workspace.yaml | 28 + scripts/gen-cordis-catalog.ts | 1 + scripts/gen-third-party-notices.ts | 1 + scripts/link-community-skins.mjs | 142 +++ tsconfig.base.json | 3 + tsconfig.client.json | 1 + tsconfig.host.json | 1 + 82 files changed, 3659 insertions(+), 123 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.md create mode 100644 .agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.zh.md create mode 100644 apps/web/tests/snapshots/settings-chrome/plugin-controls.expected.md create mode 100644 packages/client/ui-settings-plugin-control/README.i18n.yaml create mode 100644 packages/client/ui-settings-plugin-control/README.md create mode 100644 packages/client/ui-settings-plugin-control/README.zh.md create mode 100644 packages/client/ui-settings-plugin-control/package.json create mode 100644 packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.module.css create mode 100644 packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.tsx create mode 100644 packages/client/ui-settings-plugin-control/src/client/index.ts create mode 100644 packages/client/ui-settings-plugin-control/src/client/locales.ts create mode 100644 packages/client/ui-settings-plugin-control/src/client/protocol.ts create mode 100644 packages/client/ui-settings-plugin-control/src/css-modules.d.ts create mode 100644 packages/client/ui-settings-plugin-control/src/index.ts create mode 100644 packages/client/ui-settings-plugin-control/src/invariant.ts create mode 100644 packages/client/ui-settings-plugin-control/tests/browser-plugin.client.spec.tsx create mode 100644 packages/client/ui-settings-plugin-control/tests/components.client.spec.tsx create mode 100644 packages/client/ui-settings-plugin-control/tests/protocol.client.spec.ts create mode 100644 packages/client/ui-settings-plugin-control/tsconfig.json create mode 100644 packages/client/ui-settings-plugin-control/tsdown.config.ts create mode 100644 packages/host/plugin-control/README.i18n.yaml create mode 100644 packages/host/plugin-control/README.md create mode 100644 packages/host/plugin-control/README.zh.md create mode 100644 packages/host/plugin-control/package.json create mode 100644 packages/host/plugin-control/src/control-file.ts create mode 100644 packages/host/plugin-control/src/index.ts create mode 100644 packages/host/plugin-control/src/invariant.ts create mode 100644 packages/host/plugin-control/src/types.ts create mode 100644 packages/host/plugin-control/tests/control.spec.ts create mode 100644 packages/host/plugin-control/tsconfig.json create mode 100644 patches/@linxin666__dsh-client-ui-skin-center.patch create mode 100644 scripts/link-community-skins.mjs diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml index 20ffb2b3e5..0416a6e768 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md -2026-08-11-plugin-settings-tabs.md: 96e4c48926c835bcc3e6b5bf2d59126a999fbb2b -2026-08-11-plugin-settings-tabs.zh.md: fa8f4626408316fd1afbfa1ab4b4cab14d69c538 +2026-08-11-plugin-settings-tabs.md: 950968d2878b5eca3412d0db884abe9708f51f10 +2026-08-11-plugin-settings-tabs.zh.md: 9f0a8416fca48bc60c630816b51a088a2d60a9d3 diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md index 96e4c48926..950968d287 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md @@ -14,6 +14,8 @@ Plugin configuration and the read-only Loader inventory each registered a top-le The section owner contributes a `configurable` tab that declares the existing nested `settings.plugin.item` list. Configuration cards keep their namespace bindings, draft state, validation, and writes unchanged. `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` contributes an `all` tab to `settings.plugins.tab`; its Host Loader observer, generated Remote namespace, DTO, and search semantics remain unchanged. Disabled inventory entries omit the redundant unmounted runtime state from summaries and details, while enabled entries continue to expose their Cordis phase. +`@deepseek-ai/dsh-client-ui-settings-plugin-control` later joined the same slot with the `controls` tab described by the [built-in community plugins and profile controls decision](2026-08-14-built-in-community-plugins-and-controls.md). Its loopback-only mutation path remains separate from the read-only inventory Remote: the control catalog is deployment-owned, persists profile patches, and does not turn arbitrary inventory rows into mutable entries. + The first ordered tab is selected by default. A tab mounts only when first selected and then remains mounted but hidden while the Plugins section stays mounted. This delays the inventory RPC until the user opens **Plugin list** and preserves drafts, search text, disclosure state, and the fetched snapshot while switching tabs. Closing Settings unmounts the section, so reopening it obtains a fresh inventory snapshot when that tab is selected again. Both registrations use `ctx.slots.inject()`. If the section declarer unloads, the tab declaration and every contribution collapse with it; redeclaration lets each feature re-register without a static import or activation-order dependency. @@ -30,8 +32,8 @@ Both registrations use `ctx.slots.inject()`. If the section declarer unloads, th ## Consequences -Settings has one Plugins navigation row, ordered before Agent Presets, with **Plugin configuration** and **Plugin list** tabs. Agent Presets remains an independent section because it edits per-session agent compositions rather than the live Host Loader tree. +Settings has one Plugins navigation row, ordered before Agent Presets, with **Plugin configuration**, **Plugin list**, and **Plugin switches** tabs. Agent Presets remains an independent section because it edits per-session agent compositions rather than the live Host Loader tree. -Feature ownership remains explicit: `ui-settings-plugins` owns the Plugins page and editable cards, `ui-settings-plugin-inventory` owns the read-only inventory view, and the Host/RPC path does not change. A new Plugins view can join by registering one `settings.plugins.tab` contribution. +Feature ownership remains explicit: `ui-settings-plugins` owns the Plugins page and editable cards, `ui-settings-plugin-inventory` owns the read-only inventory view, and `ui-settings-plugin-control` owns the deployment-scoped switches. A new Plugins view can join by registering one `settings.plugins.tab` contribution. The aggregation depends on the section owner being composed: without `ui-settings-plugins`, `ui-settings-plugin-inventory` waits for a tab declaration and renders nothing. That is an intentional composition dependency carried by the slot registry rather than a static package import. diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md index fa8f462640..9f0a8416fc 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md @@ -14,6 +14,8 @@ Status: implemented 分区拥有方贡献 `configurable` 标签页,由它声明既有的嵌套 `settings.plugin.item` 列表。配置卡片原有的命名空间绑定、草稿状态、校验与写入均保持不变。`@deepseek-ai/dsh-client-ui-settings-plugin-inventory` 向 `settings.plugins.tab` 贡献 `all` 标签页;它的 Host Loader 观察器、生成的 Remote 命名空间、DTO 与搜索语义保持不变。已停用的清单条目会在摘要和详情中省略重复的“未挂载”运行状态,已启用条目仍显示其 Cordis 阶段。 +随后,`@deepseek-ai/dsh-client-ui-settings-plugin-control` 依据[内置社区插件与 profile 开关决策](2026-08-14-built-in-community-plugins-and-controls.md)以 `controls` 标签页加入同一 slot。它的仅限回环修改路径与只读清单 Remote 保持分离:控制清单由部署方拥有,负责持久化 profile patch,并不会让任意清单行变成可修改条目。 + 默认选择顺序中的第一个标签页。某个标签页只有首次被选择时才挂载,之后在“插件”分区保持挂载期间只隐藏而不卸载。这样会把清单 RPC 延迟到用户打开**插件列表**时,并在切换标签页时保留草稿、搜索文本、折叠状态和已读取的快照。关闭 Settings 会卸载该分区,因此再次打开后,重新选择该标签页时会取得新的清单快照。 两项注册都使用 `ctx.slots.inject()`。分区声明方卸载时,标签 slot 及其全部贡献随之折叠;重新声明后,每项功能都能重新注册,无需静态 import,也不依赖激活顺序。 @@ -30,8 +32,8 @@ Status: implemented ## 影响 -Settings 只有一行“插件”导航,排在“Agent 预设”之前,包含**插件配置**与**插件列表**两个标签页。“Agent 预设”仍是独立分区,因为它编辑每个会话的 agent 组装,而非实时 Host Loader 树。 +Settings 只有一行“插件”导航,排在“Agent 预设”之前,包含**插件配置**、**插件列表**与**插件开关**三个标签页。“Agent 预设”仍是独立分区,因为它编辑每个会话的 agent 组装,而非实时 Host Loader 树。 -功能所有权保持明确:`ui-settings-plugins` 拥有“插件”页面与可编辑卡片,`ui-settings-plugin-inventory` 拥有只读清单视图,Host/RPC 路径不变。新的“插件”视图只需注册一个 `settings.plugins.tab` 贡献即可加入。 +功能所有权保持明确:`ui-settings-plugins` 拥有“插件”页面与可编辑卡片,`ui-settings-plugin-inventory` 拥有只读清单视图,`ui-settings-plugin-control` 拥有部署范围的开关。新的“插件”视图只需注册一个 `settings.plugins.tab` 贡献即可加入。 该聚合依赖分区拥有方被组装:没有 `ui-settings-plugins` 时,`ui-settings-plugin-inventory` 会等待标签 slot 的声明且不渲染任何内容。这是通过 slot 注册表承载的有意组合依赖,而不是静态包 import。 diff --git a/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.i18n.yaml new file mode 100644 index 0000000000..ccee1bd6ea --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.i18n.yaml @@ -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 .agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.md +2026-08-14-built-in-community-plugins-and-controls.md: 31dd5c16e716c66655d9278924ee8430436c43e6 +2026-08-14-built-in-community-plugins-and-controls.zh.md: 85066a2492f28d2c3e189cf0d460cdd6392f628b diff --git a/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.md b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.md new file mode 100644 index 0000000000..31dd5c16e7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.md @@ -0,0 +1,51 @@ +# Agent Note: Built-in community plugins and profile-scoped controls + +Status: implemented + +English | [中文](2026-08-14-built-in-community-plugins-and-controls.zh.md) + +## Problem + +The shipped Web profile contained only the repository-owned base and Web bundles. Users who wanted generative UI, selection annotations, or the community Web UI collection had to discover and install each package themselves, while this distribution intended those three products to be part of its default experience. Settings exposed configuration and a read-only Loader inventory but had no narrow control surface for disabling a distribution-owned product without editing YAML. + +A general Loader mutation endpoint would make every installed row remotely addressable, conflate deployment policy with inventory, and inherit teardown behavior from third-party plugins. Live disable/re-enable is not safe as a universal promise: a plugin may register routes, tools, or DOM resources without returning lifecycle disposers, so reactivation can collide with registrations left by the first activation. + +## Decision + +The `web` profile template orders five bundles: `@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@omdsh-dev/dsh-genui`, `@omdsh-dev/dsh-annotation`, and `@linxin666/dsh-web-ui-all`. The two Git packages are pinned to exact commits and the npm aggregate is pinned to `0.1.2`; the lockfile is the source acquisition record. The `dsh-web-app` bundle also declares the aggregate's nine entry packages and the whale-song skin package as direct dependencies pinned to the same `0.1.2`: pnpm's isolated layout does not place nested dependencies on the bundle package's own `node_modules`, and the profile module fallback only mirrors packages resolvable from each closure anchor, so only direct declarations make those entry rows resolvable from a profile directory. The exact former base-plus-Web tuple is installation-owned and migrates to the five-bundle template, while any customized bundle list remains user-owned. This uses the existing profile bundle mechanism and does not restore the removed repository-Plugin path. + +The dsh-web-ui 0.1.2 skin center assumes a `skins/` directory in its checkout layout (`packages/skins/`), which no bundled deployment provides, so both try-on and apply fail with ENOENT; its 0.1.2 aggregate also ships no skin rows in the bundle layer (`skin.json`'s `bundleWired: true` contradicts the npm publish), so even a restart would never mount the managed rows. This distribution bridges the upstream gap in four places: a `patchedDependencies` patch to `@linxin666/dsh-client-ui-skin-center` makes it walk ancestor directories for `skins/` when the original location is absent; the same patch makes the managed section always insert the active skin's row and, after a successful apply, reconciles the running Loader tree live (mount the active skin row, disable the others) — packaged Electron cannot provide Cordis HMR's loader internals (`node-addon-require-builtin` fails under Electron) and the desktop disables the patch watcher, so the live reconcile is the only path that makes apply take effect immediately; `scripts/link-community-skins.mjs` links the installed skin packages into the workspace-root `node_modules/skins/` at postinstall time (source launches); and desktop packaging stages the same set into `skins-extras`, shipped by `extraResources` to `app/node_modules/skins` (the packaged app). whale-song is published but missing from the aggregate's dependency list while the 0.1.2 client registry already lists it, so declaring that package directly makes all seven skin-center cards usable. + +The Web bundle declares three logical controls: one Loader row for GenUI, one for Annotation, and nine rows moved together as dsh-web-ui. `@deepseek-ai/dsh-host-plugin-control` exposes `list` and `set-enabled` on a loopback-only generic Connection channel. Its deployment-owned catalog is the complete mutation allowlist; profile-local ids must each resolve to exactly one mounted Loader entry. The gateway never accepts an arbitrary inventory id and remains separate from the read-only Typert inventory Remote. + +Each mutation writes managed `{id, disabled}` patches to the active profile's `cordis.patch.yml` under `# dsh-plugin-control: ` comments. File locking and atomic publication serialize concurrent writers and preserve unrelated YAML nodes, comments, and `!!js` expressions. The launcher provides the exact profile patch path as `ctx.profileUserPatchPath` before rows mount, so the Host plugin does not derive a path from ambient home state. + +Switches are restart-time settings. The gateway returns the saved desired state but does not mutate the current Loader tree; the next process applies the ordinary profile layer order. This supports third-party plugins without claiming reversible teardown. Home-level patches and command-line overlays retain their later-layer precedence. + +`@deepseek-ai/dsh-client-ui-settings-plugin-control` contributes the third `settings.plugins.tab` entry, **Plugin switches**, through the existing slot ledger. It lazily reads state only when selected, renders one accessible switch per logical product with source attribution, never calls the privileged route from a remote browser, and tells the user that a successful change requires restart. + +## Alternatives considered + +**Copy or fork the three projects into this repository.** Rejected because the requested projects already publish installable profile bundles. Pinning their upstream packages preserves attribution and lets their owners retain implementation and release responsibility. + +**Expose enable/disable on every row in Plugin list.** Rejected because inventory does not identify which rows form one product, and an unrestricted mutation endpoint would expand browser authority from three distribution choices to the complete deployment tree. + +**Apply Loader updates immediately after writing the patch.** Rejected after the real Web composition demonstrated a valid third-party lifecycle that could disable but not reactivate without a duplicate route. A restart-time rule is deterministic for all profile bundles and avoids partially reloaded products. + +**Store settings in a separate JSON file.** Rejected because the profile patch is already the authoritative user-owned layer, participates in dump and HMR semantics, and makes the next boot inspectable without another configuration source. + +**Use Typert for mutations.** Rejected because the existing generated inventory namespace is deliberately read-only and transport-neutral. The generic Connection channel already owns trust-scoped browser-to-Host commands without broadening the API graph. + +## Consequences + +New and installation-owned Web profiles receive all three community products enabled by default. Existing profiles with the exact previously shipped tuple migrate automatically; customized profiles do not gain surprise layers. The source packages and LINUX DO receive visible acknowledgements in both root READMEs, while generated third-party notices carry their licenses. + +Settings retains one Plugins navigation row and now has three independently owned tabs. The earlier feature-owned tab decision remains the slot architecture authority; this note extends its concrete roster and adds a separate privileged capability. + +The profile-bundle and repository-Plugin notes remain active foundation records. This feature uses ordered bundle dependencies as the single external distribution path and adds no source cache, wrapper format, or second installer. + +GenUI and dsh-web-ui can change model-visible prompts and tools, while Annotation adds model-visible content when used. Disabling a product therefore changes the next process's request prefix or tool roster and begins a new KV-cache prefix after restart. + +## Testing + +Focused Host tests cover catalog validation, loopback registration, aggregate states, serialized atomic YAML writes, cancellation, invalid YAML, unavailable controls, and preservation of unrelated nodes. Browser package tests cover slot lifecycle, localization, response validation, accessibility, remote authority, retry, mutation failure, and late settlements. Profile tests cover the five-bundle template plus exact legacy migration and customized-list preservation. The keyless Web browser replay boots the real five-bundle composition, snapshots the third tab, writes both switch states through the real loopback route, confirms the running plugin remains mounted, and verifies the managed profile patches. diff --git a/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.zh.md b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.zh.md new file mode 100644 index 0000000000..85066a2492 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-14-built-in-community-plugins-and-controls.zh.md @@ -0,0 +1,51 @@ +# Agent Note: 内置社区插件与 profile 范围开关 + +Status: implemented + +[English](2026-08-14-built-in-community-plugins-and-controls.md) | 中文 + +## 问题 + +随发行版提供的 Web profile 原先只包含仓库自有的 base 与 Web 组合包。想使用生成式 UI、文本选择批注或社区 Web UI 集合的用户,必须自行发现并安装每个包,但本发行版希望把这三个产品作为默认体验的一部分。Settings 已提供配置与只读 Loader 清单,却没有一个范围受限的控制界面,让用户无需编辑 YAML 即可停用发行版自有产品。 + +通用 Loader 修改端点会使每个已安装行都能被远程寻址,把部署策略与清单混为一体,并继承第三方插件的 teardown 行为。实时停用再启用不能作为普遍承诺:插件可能注册路由、工具或 DOM 资源,却不返回生命周期 disposer;再次激活就可能与首次激活遗留的注册冲突。 + +## 决策 + +`web` profile 模板按顺序包含五个组合包:`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@omdsh-dev/dsh-genui`、`@omdsh-dev/dsh-annotation` 和 `@linxin666/dsh-web-ui-all`。两个 Git 包锁定到确切 commit,npm 聚合包锁定到 `0.1.2`;lockfile 记录源码获取结果。`dsh-web-app` 组合包还直接把聚合包的九个入口包与 whale-song 皮肤包声明为同样锁定 `0.1.2` 的直接依赖:pnpm 的隔离布局不会把嵌套依赖放到组合包自身的 `node_modules` 上,而 profile 模块回退目录只镜像安装闭包中可从每个锚点解析的包,因此只有直接声明才能让 Loader 从 profile 目录解析这些入口行。旧的精确 base 加 Web 元组归安装所有,并会迁移到五组合包模板;任何自定义 bundle 列表仍归用户所有。本功能使用既有 profile bundle 机制,不会恢复已移除的 repository-Plugin 路径。 + +dsh-web-ui 0.1.2 的皮肤中心假设 `skins/` 目录位于其 checkout 布局里(`packages/skins/`),任何打包部署都没有这个目录,因此试穿与应用都会以 ENOENT 失败;其 0.1.2 聚合包也没有把皮肤行装进组合层(`skin.json` 的 `bundleWired: true` 与 npm 发布内容不符),所以即使重启,受管区段也不会挂载任何皮肤行。本发行版以四处弥合这个上游缺口:用 `patchedDependencies` 给 `@linxin666/dsh-client-ui-skin-center` 打补丁,让它在原始位置不存在时沿祖先目录向上查找 `skins/`;补丁同时让受管区段总是插入当前皮肤的插入行,并在应用接口成功后直接对运行中的 Loader 树做 live reconcile(挂载当前皮肤行、禁用其余行)——打包 Electron 无法提供 Cordis HMR 的 loader internals(`node-addon-require-builtin` 在 Electron 下不可用),而桌面版禁用 patch watcher,所以 live reconcile 是应用即时生效的唯一通道;`scripts/link-community-skins.mjs` 在 postinstall 时把安装好的皮肤包链接成工作区根 `node_modules/skins/`(源码启动用);桌面打包在 stage 时把同样一组皮肤装进 `skins-extras`,经 `extraResources` 落到 `app/node_modules/skins`(打包应用用)。whale-song 已发布但不在聚合包依赖里,而 0.1.2 的客户端注册表已列出它,因此把该包声明为直接依赖后皮肤中心七张卡片全部可用。 + +Web 组合包声明三个逻辑控制项:GenUI 的一个 Loader 行、Annotation 的一个 Loader 行,以及作为 dsh-web-ui 整体移动的九个行。`@deepseek-ai/dsh-host-plugin-control` 在仅限回环访问的通用 Connection 通道上暴露 `list` 与 `set-enabled`。由部署方拥有的清单就是完整的修改允许列表;每个 profile 本地 id 必须恰好解析为一个已挂载 Loader 条目。gateway 从不接受任意清单 id,并与只读 Typert 清单 Remote 保持分离。 + +每次修改都会把受管 `{id, disabled}` patch 写入当前 profile 的 `cordis.patch.yml`,并用 `# dsh-plugin-control: ` 注释标记。文件锁与原子发布会串行处理并发写入,并保留无关 YAML 节点、注释及 `!!js` 表达式。启动器会在配置行挂载前以 `ctx.profileUserPatchPath` 提供确切的 profile patch 路径,因此 Host 插件不需要从环境中的 home 状态推导路径。 + +开关属于重启时设置。gateway 返回已保存的期望状态,但不修改当前 Loader 树;下一个进程通过普通 profile 层顺序应用设置。这样既能支持第三方插件,也不声称其 teardown 可逆。home 级 patch 与命令行 overlay 仍保留后应用的优先级。 + +`@deepseek-ai/dsh-client-ui-settings-plugin-control` 通过既有 slot 记录贡献第三个 `settings.plugins.tab` 条目:**插件开关**。它只在首次选择时懒读取状态,为每个逻辑产品渲染一项带源码归属的可访问开关,远程浏览器绝不调用特权路由,并明确告知用户更改成功后需要重启。 + +## 备选方案 + +**把三个项目复制或 fork 到本仓库。** 否决,因为用户指定的项目已经发布可安装的 profile 组合包。锁定其上游包可以保留归属,并让项目所有者继续负责实现与发布。 + +**在“插件列表”中为每一行暴露启用/停用。** 否决,因为清单无法说明哪些行共同组成一个产品,而不受限的修改端点会把浏览器权限从三个发行版选择扩大到整个部署树。 + +**写入 patch 后立即更新 Loader。** 真实 Web 组合证明一种有效的第三方生命周期可以停用却无法重新激活,并会产生重复路由,因此否决。重启时规则对所有 profile 组合包都具有确定性,也避免产品处于部分重载状态。 + +**把设置存入独立 JSON 文件。** 否决,因为 profile patch 已是权威的用户自有层,会参与 dump 与 HMR 语义,并使下次启动无需引入另一配置来源即可检查。 + +**使用 Typert 执行修改。** 否决,因为既有生成清单命名空间刻意保持只读且与传输无关。通用 Connection 通道已经承载按信任范围限制的浏览器到 Host 命令,无需扩大 API 图。 + +## 影响 + +新的、归安装所有的 Web profile 会默认获得并启用三个社区产品。与旧随附元组完全一致的现有 profile 会自动迁移;自定义 profile 不会意外新增配置层。根目录双语 README 会明确致谢源码包与 LINUX DO,生成的第三方声明则记录其许可证。 + +Settings 仍只有一行“插件”导航,现在包含三个所有权独立的标签页。此前的功能自有标签页决策仍是 slot 架构权威;本记录扩展其具体名录并增加一项独立的特权能力。 + +profile 组合包与移除 repository-Plugin 的记录仍是有效基础。本功能使用有序 bundle 依赖作为唯一外部分发路径,不增加源码缓存、包装格式或第二套安装器。 + +GenUI 与 dsh-web-ui 可以改变模型可见的提示词与工具,Annotation 则在使用时加入模型可见内容。因此停用产品会改变下一个进程的请求前缀或工具名录,并在重启后开始新的 KV cache 前缀。 + +## 测试 + +Host 聚焦测试覆盖清单校验、回环注册、聚合状态、串行原子 YAML 写入、取消、无效 YAML、不可用控制项,以及无关节点保留。浏览器包测试覆盖 slot 生命周期、本地化、响应校验、可访问性、远程权限、重试、修改失败与延迟结算。Profile 测试覆盖五组合包模板、精确旧版本迁移与自定义列表保留。无密钥 Web 浏览器回放会启动真实五组合包组合、快照第三个标签页、通过真实回环路由写入开关的两个状态、确认运行中插件保持挂载,并校验受管 profile patch。 diff --git a/.gitattributes b/.gitattributes index e0a432111d..c4d347f2f4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,11 @@ *.pdf binary +# pnpm patch files mirror the patched file verbatim; their inner added lines +# legitimately begin with the diff marker plus a tab, which git's +# space-before-tab check would otherwise flag as a whitespace error. +*.patch whitespace=-space-before-tab + # Pairing records are generated from the three confirmed owner-blob pairs. # The worktree-local installer registers the fail-closed driver command. *.i18n.yaml merge=dsh-translation-pairing diff --git a/README.i18n.yaml b/README.i18n.yaml index f1e745a427..1b85d12c35 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write README.md -README.md: 079b40cfdebaca2f037a4563b8307fd07c908a08 -README.zh.md: 5ab67305dd9161762f88940e32a20bd47f392681 +README.md: 57bf93560762664ec32a4ad4d02438616a6bd80c +README.zh.md: 36f2f65ab21027856d841b8b6c1eef3ecdb8f511 diff --git a/README.md b/README.md index 079b40cfde..57bf935607 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,13 @@ DSHCode is an independent community project. Unless DeepSeek grants explicit aut Start with the [development guide](docs/development.md), [architecture documentation](docs/architecture.md), and [desktop application guide](apps/desktop/README.md). For agents, follow [AGENTS.md](AGENTS.md). +## Acknowledgements + +- [LINUX DO](https://linux.do) — This project is continuously shared and discussed in the LINUX DO community. +- [dsh-genui](https://github.com/omdsh-dev/dsh-genui) — Provides the built-in generative UI plugin. +- [dsh-annotation](https://github.com/omdsh-dev/dsh-annotation) — Provides the built-in selection-annotation workflow. +- [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) — Provides the built-in Web UI feature and skin collection. + ## License and branding The source remains available under the upstream [MIT License](LICENSE). Redistribution must retain DeepSeek's copyright and permission notice; bundled third-party software and licenses are disclosed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), and the desktop package includes both files. diff --git a/README.zh.md b/README.zh.md index 5ab67305dd..36f2f65ab2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -63,6 +63,13 @@ DSHCode 是独立的社区项目。除非 DeepSeek 明确授权,否则它不 请先阅读[开发指南](docs/development.md)、[架构文档](docs/architecture.md)和[桌面应用指南](apps/desktop/README.md)。面向 agent:请遵循 [AGENTS.md](AGENTS.md)。 +## 致谢 + +- [LINUX DO](https://linux.do) — 本项目也在 LINUX DO 社区持续分享与交流。 +- [dsh-genui](https://github.com/omdsh-dev/dsh-genui) — 为内置生成式 UI 能力提供插件实现。 +- [dsh-annotation](https://github.com/omdsh-dev/dsh-annotation) — 为内置文本批注流程提供插件实现。 +- [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) — 为内置 Web UI 功能与皮肤集合提供插件实现。 + ## 许可证与品牌 源码继续使用上游 [MIT 许可证](LICENSE)。再次分发时必须保留 DeepSeek 的版权与许可声明;内置第三方软件及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md),桌面安装包会同时附带这两个文件。 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 05ca140552..c4a2cd7766 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -38,7 +38,20 @@ External packages that a workspace package resolves at runtime. The tier covers | [`@earendil-works/pi-ai`](https://github.com/earendil-works/pi) | MIT | | [`@joplin/turndown-plugin-gfm`](https://github.com/laurent22/joplin-turndown-plugin-gfm) | MIT | | [`@jridgewell/gen-mapping`](https://github.com/jridgewell/sourcemaps) | MIT | +| [`@linxin666/dsh-client-ui-aionui-panel`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-client-ui-git-graph`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-client-ui-skin-center`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-client-ui-skin-whale-song`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-client-ui-task-board`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-client-ui-web-ui-settings`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-live-stats`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-pet`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-remote-web-ui`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-ssh`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | +| [`@linxin666/dsh-web-ui-all`](https://github.com/zhu1090093659/dsh-web-ui) | BSD-3-Clause | | [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) | MIT | +| [`@omdsh-dev/dsh-annotation`](https://github.com/omdsh-dev/dsh-annotation) | MIT | +| [`@omdsh-dev/dsh-genui`](https://github.com/omdsh-dev/dsh-genui) | MIT | | [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | @@ -94,6 +107,7 @@ External packages that a workspace package resolves at runtime. The tier covers pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification: +- `@linxin666/dsh-client-ui-skin-center` — [`patches/@linxin666__dsh-client-ui-skin-center.patch`](patches/@linxin666__dsh-client-ui-skin-center.patch) - `node-pty@1.1.0` — [`patches/node-pty@1.1.0.patch`](patches/node-pty@1.1.0.patch) ## Official Claude Code platform payloads diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index f270fdf882..37387cee5b 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md -README.md: 9a8d722b044ed5d8e31e3c27e54f8c9ef0839f82 -README.zh.md: 7889b4180b5ce2f65550f98708d498f324ac349d +README.md: 16dbc59f416cd649ffcf279a512b659d52bc8905 +README.zh.md: 08ba302fc85739a146b387152ce5e06b62f3d625 diff --git a/apps/cli/README.md b/apps/cli/README.md index 9a8d722b04..16dbc59f41 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -13,7 +13,7 @@ The `dsh` command is the product launcher for profiles: ordered stacks of plugin | `dsh web` | Alias of `--profile web`. | | `dsh plugin --profile ` | Manage a profile's plugins by forwarding to pnpm in the profile directory. | -The invoking directory is the default workspace root. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. +The invoking directory is the default workspace root. The `web` and `headless` profiles auto-initialize on first use from shipped templates; any other profile must be created through `dsh plugin`. The shipped Web template includes dsh-genui, dsh-annotation, and the dsh-web-ui aggregate, all enabled by default and controllable from Settings after restart. ## App arguments @@ -36,7 +36,7 @@ The tree composes over an empty root: - then the profile's `cordis.patch.yml`, then the home-level `$DSH_HOME/cordis.patch.yml` - then `--patch` overlays -Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (`@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. +Bundles named in `dsh.profile.bundles` resolve from the dsh installation first (including `@deepseek-ai/dsh-base`, `@deepseek-ai/dsh-web-app`, `@deepseek-ai/dsh-headless`, and the three community Web bundles shipped by this distribution), then from the profile's own `node_modules`, where pnpm installs out-of-tree plugins. Use `--dump-default-config` and `--dump-config` to inspect the composed tree without booting it. diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 7889b4180b..08ba302fc8 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -13,7 +13,7 @@ | `dsh web` | `--profile web` 的别名。 | | `dsh plugin --profile ` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 | -运行命令时所在的目录将作为默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 +运行命令时所在的目录将作为默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。随附的 Web 模板包含 dsh-genui、dsh-annotation 与 dsh-web-ui 聚合包,三者均默认启用,并可在 Settings 中切换后经重启生效。 ## 应用参数 @@ -36,7 +36,7 @@ profile 目录包含一个 `package.json`,其中记录树外插件依赖,以 - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` - `--patch` 指定的覆盖层 -`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 +`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(包括 `@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`,以及本发行版随附的三个社区 Web 组合包),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 50e0d9c97f..10f777e660 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -252,6 +252,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con // Before any config-tree entry mounts, so plugins resolve all launch-time // environment values from the same immutable provenance snapshot. hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment) + // Profile-aware plugins persist composition changes into this invocation's + // own user layer instead of assuming the shipped `web` profile name. + hostCtx.provide('profileUserPatchPath', composed.profile.patchPath) // The command line and bounded exit request are launcher facts available // to every app plugin that injects the argument snapshot. provideCmdline(hostCtx, { diff --git a/apps/desktop/electron-builder.yml b/apps/desktop/electron-builder.yml index 1c7c5d14dc..83f8ebf8bd 100644 --- a/apps/desktop/electron-builder.yml +++ b/apps/desktop/electron-builder.yml @@ -17,6 +17,12 @@ files: extraResources: - from: licenses to: licenses + # The deployment-owned skins tree the skin center reads at runtime. The + # node-module collector would drop an orphan directory inside node_modules, + # so the staged skins-extras assembly ships through extraResources into the + # exact ancestor the patched skin center resolves. + - from: skins-extras + to: app/node_modules/skins mac: category: public.app-category.developer-tools identity: null diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d45596e02c..ba0197f457 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -73,6 +73,7 @@ "@deepseek-ai/dsh-home-paths": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^", "@deepseek-ai/dsh-host-directory-picker-native": "workspace:^", + "@deepseek-ai/dsh-host-plugin-control": "workspace:^", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/apps/desktop/scripts/prepare-package.mjs b/apps/desktop/scripts/prepare-package.mjs index e43a41d546..fe3143f5c5 100644 --- a/apps/desktop/scripts/prepare-package.mjs +++ b/apps/desktop/scripts/prepare-package.mjs @@ -1,9 +1,9 @@ /** Stage a production-only workspace deployment for electron-builder. */ -import { copyFileSync, mkdirSync, rmSync } from 'node:fs' +import { copyFileSync, existsSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync, unlinkSync, lstatSync } from 'node:fs' import { createHash } from 'node:crypto' import { tmpdir } from 'node:os' -import { resolve } from 'node:path' +import { dirname, join, resolve } from 'node:path' import { spawnSync } from 'node:child_process' const appRoot = resolve(import.meta.dirname, '..') @@ -22,6 +22,50 @@ function runPnpm(args) { }) } +/** + * Assemble the deployment-owned skins tree the skin center reads: + * `/skins-extras/` links to the staged skin packages. The + * patched `@linxin666/dsh-client-ui-skin-center` resolves `skins/` beside + * an ancestor `node_modules`, and electron-builder ships this tree at + * `app/node_modules/skins` through `extraResources` (the node-module + * collector would drop an orphan directory inside `node_modules`). + * pnpm's staged layout keeps the skin packages inside the virtual store, + * so the scan walks `node_modules/.pnpm` store entries and keeps only the + * packages carrying a `skin.json` (the skin-center plugin itself shares + * the package-name prefix but is not a skin). + * @returns the number of staged skins. + */ +function assembleSkinsExtras() { + const prefix = '@linxin666+dsh-client-ui-skin-' + const pnpmDir = join(stageRoot, 'node_modules', '.pnpm') + const extrasRoot = join(stageRoot, 'skins-extras') + mkdirSync(extrasRoot, { recursive: true }) + let staged = 0 + for (const entry of readdirSync(pnpmDir)) { + if (!entry.startsWith(prefix)) continue + const rest = entry.slice(prefix.length) + const at = rest.indexOf('@') + const id = at === -1 ? rest : rest.slice(0, at) + const target = join(pnpmDir, entry, 'node_modules', '@linxin666', `dsh-client-ui-skin-${id}`) + if (!existsSync(join(target, 'skin.json'))) continue + const link = join(extrasRoot, id) + try { + const stat = lstatSync(link) + if (!stat.isSymbolicLink()) { + throw new Error(`DSHCode packaging: ${link} exists and is not a symlink; remove it so the skins tree can be staged`) + } + if (readlinkSync(link) === target) continue + unlinkSync(link) + } catch (error) { + if (error?.code !== 'ENOENT') throw error + } + mkdirSync(dirname(link), { recursive: true }) + symlinkSync(target, link, 'junction') + staged += 1 + } + return staged +} + /** Create a production deployment outside the source workspace for electron-builder. */ export function preparePackage() { const verified = runPnpm(['run', 'verify-desktop-runtime-closure']) @@ -53,6 +97,8 @@ export function preparePackage() { const licenseRoot = resolve(stageRoot, 'licenses') mkdirSync(buildRoot, { recursive: true }) mkdirSync(licenseRoot, { recursive: true }) + const skinCount = assembleSkinsExtras() + console.log(`DSHCode packaging: ${skinCount} skin(s) staged at skins-extras`) copyFileSync(resolve(appRoot, 'electron-builder.yml'), resolve(stageRoot, 'electron-builder.yml')) copyFileSync(resolve(appRoot, 'assets/icon.svg'), resolve(buildRoot, 'icon.svg')) copyFileSync(resolve(workspaceRoot, 'LICENSE'), resolve(licenseRoot, 'LICENSE')) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index f1ba62d881..65f8ceb5ee 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -1,7 +1,7 @@ // Shared scaffold for the keyless browser e2e lane (Agent Note: // .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md). -// Boots the REAL web composition — the dsh-base and dsh-web-app bundle -// patches over the empty profile root through the vendored Loader (the same +// Boots the REAL web composition — every bundle patch in the shipped Web +// profile over the empty profile root through the vendored Loader (the same // layer stack the profile boot composes), patched the // snapshot way — so a real chromium exercises the real HTTP uplink/WebSocket // downlink, api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT: @@ -39,6 +39,7 @@ import { composeEntries, healProfilesModuleFallback, loadOverlayPatches, + resolveBundleDir, } from '@deepseek-ai/dsh-app-boot' import { dshHomePath } from '@deepseek-ai/dsh-home-paths' import { settingsNamespace } from '@deepseek-ai/dsh-settings' @@ -96,9 +97,15 @@ export function webSnapshotMode(): WebSnapshotMode { throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`) } -/** The shipped composition under test: the dsh-base and dsh-web-app bundle patches over the empty profile root. */ +/** The first two layers of the shipped Web composition: base and Web application bundle patches. */ const BASE_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml') const WEB_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml') +/** Community bundle layers shipped after dsh-web-app in the Web profile. */ +const COMMUNITY_BUNDLES = [ + '@omdsh-dev/dsh-genui', + '@omdsh-dev/dsh-annotation', + '@linxin666/dsh-web-ui-all', +] as const /** The installation anchor whose dependency surface the profile module fallback mirrors. */ const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json') /** The deployment's own agent-preset root, shipped beside the app's config. */ @@ -177,6 +184,8 @@ export interface WebScaffold { persistenceRoot: string /** Isolated harness home the settings/credentials rows write ($DSH_HOME double). */ harnessHome: string + /** Current profile's user patch file used by plugin-control persistence. */ + profilePatchPath: string /** Await a settled turn end: in-process turn/end, then the agent's idle flip (which follows the persistence flush). */ whenTurnSettled(timeoutMs?: number): Promise /** Tear everything down; asserts the replay fixture was fully consumed first (replay/refresh). */ @@ -368,10 +377,23 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise { + const packageDir = resolveBundleDir('web e2e scaffold', packageName, INSTALL_ANCHOR, profileDir) + const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8')) as { + dsh?: { bundle?: { patch?: string } } + } + const relativePatch = manifest.dsh?.bundle?.patch + if (relativePatch === undefined) { + throw new Error(`web e2e scaffold: community bundle ${packageName} declares no patch`) + } + return loadOverlayPatches('web e2e scaffold', join(packageDir, relativePatch)) + }))).flat() const extraOverlayPatches = options.extraOverlayPath === undefined ? [] : loadOverlayPatches('web e2e scaffold', options.extraOverlayPath) - const composedRows = composeEntries([basePatches, surfacePatches, extraOverlayPatches]) + const composedRows = composeEntries([basePatches, surfacePatches, communityPatches, extraOverlayPatches]) const webRuntimeConfig = composedRows.find(row => row.id === 'web-runtime')?.config as { surfaceContext?: boolean } | undefined @@ -379,6 +401,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise/profiles. healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome) - const profileDir = join(harnessHome, 'profiles', 'scaffold') await mkdir(profileDir, { recursive: true }) const rootConfig = join(profileDir, 'cordis.yml') + const profilePatchPath = join(profileDir, 'cordis.patch.yml') await writeFile(rootConfig, '[]\n') + await writeFile(profilePatchPath, '[]\n') ctx.baseUrl = pathToFileURL(profileDir).href + '/' // This direct Loader harness supplies the same root-path capability as app-boot. ctx.provide('dshHomePath', dshHomePath) + ctx.provide('profileUserPatchPath', profilePatchPath) // A host with no command line still provides one: the web bundle's startup // row releases the rows waiting on it, and with no arguments each starts on // the values this scaffold composed above. An exit request can only come @@ -581,6 +606,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise { @@ -129,6 +131,58 @@ describe('web e2e: settings modal and General preferences', () => { expect(tripwire.pageErrors).toEqual([]) }, 60_000) + it('persists a restart-time built-in plugin switch from the third Plugins tab', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-plugin-control')) + await page.getByRole('button', { name: '设置', exact: true }).click() + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.getByRole('button', { name: '插件', exact: true }).click() + const tab = dialog.getByRole('tab', { name: '插件开关', exact: true }) + await tab.waitFor({ timeout: 10_000 }) + await tab.click() + expect(await tab.getAttribute('aria-selected')).toBe('true') + + const panel = dialog.locator(PLUGIN_CONTROL_PANEL_SELECTOR) + await panel.waitFor({ timeout: 10_000 }) + await expect.poll(() => panel.locator('[data-plugin-control]').count(), { timeout: 10_000 }).toBe(3) + expect(await panel.locator('[data-plugin-control="genui"] strong').textContent()).toBe('dsh-genui') + expect(await panel.locator('[data-plugin-control="annotation"] strong').textContent()).toBe('dsh-annotation') + expect(await panel.locator('[data-plugin-control="web-ui"] strong').textContent()).toBe('dsh-web-ui') + const snapshot = await captureStableAria(page, PLUGIN_CONTROL_PANEL_SELECTOR, scaffold.workspaceCwd) + await compareOrRefreshGolden(PLUGIN_CONTROLS_EXPECTED, snapshot, MODE) + + const row = panel.locator('[data-plugin-control="genui"]') + const pluginSwitch = row.getByRole('switch') + expect(await pluginSwitch.getAttribute('aria-checked')).toBe('true') + const disableResponse = page.waitForResponse(response => response.url().includes('/plugin-control/set-enabled')) + await pluginSwitch.click() + const disableResponseText = await (await disableResponse).text() + if (!disableResponseText.includes('"ok":true')) { + throw new Error(`plugin-control disable request failed: ${disableResponseText}`) + } + await expect.poll(() => [...scaffold.ctx.loader.entries()].find(entry => entry.options.id === 'genui')?.disabled, { + timeout: 10_000, + }).toBe(false) + await expect.poll(() => readFile(scaffold.profilePatchPath, 'utf8'), { timeout: 10_000 }) + .toMatch(/# dsh-plugin-control: genui[\s\S]*id: genui[\s\S]*disabled: true/) + await expect.poll(() => pluginSwitch.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('false') + expect(await panel.getByRole('status').textContent()).toBe('更改已保存,请重启 DSH 使其生效。') + + const enableResponse = page.waitForResponse(response => response.url().includes('/plugin-control/set-enabled')) + await pluginSwitch.click() + const enableResponseText = await (await enableResponse).text() + if (!enableResponseText.includes('"ok":true')) { + throw new Error(`plugin-control enable request failed: ${enableResponseText}`) + } + await expect.poll(() => [...scaffold.ctx.loader.entries()].find(entry => entry.options.id === 'genui')?.disabled, { + timeout: 10_000, + }).toBe(false) + await expect.poll(() => readFile(scaffold.profilePatchPath, 'utf8'), { timeout: 10_000 }) + .toMatch(/# dsh-plugin-control: genui[\s\S]*id: genui[\s\S]*disabled: false/) + await expect.poll(() => pluginSwitch.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('true') + await page.keyboard.press('Escape') + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + it('stores Permission as the default for future sessions without changing an existing session', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-permission')) const existing = scaffold.ctx.sessions.create(SessionId('settings-permission-before')) @@ -480,6 +534,10 @@ 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', 'plugins.expected.md']) + await assertFixtureInventory(SNAPSHOT_DIR, [ + 'dialog.expected.md', + 'plugin-controls.expected.md', + 'plugins.expected.md', + ]) }) }) diff --git a/apps/web/tests/snapshots/plugin-config/section.expected.md b/apps/web/tests/snapshots/plugin-config/section.expected.md index 54ac42b4a7..5ec9fde05e 100644 --- a/apps/web/tests/snapshots/plugin-config/section.expected.md +++ b/apps/web/tests/snapshots/plugin-config/section.expected.md @@ -22,6 +22,7 @@ - tablist "插件视图": - tab "插件配置" [selected] - tab "插件列表" + - tab "插件开关" - tabpanel "插件配置": - list: - listitem: @@ -36,3 +37,5 @@ - 'button "展开设置: 网页搜索"': - text: 网页搜索 DeepSeek 搜索提供方。 - img + - listitem: + - 'button "展开: Web UI 插件"': Web UI 插件 统一管理 dsh-web-ui 全家桶插件的启用与配置。 ▾ diff --git a/apps/web/tests/snapshots/settings-chrome/plugin-controls.expected.md b/apps/web/tests/snapshots/settings-chrome/plugin-controls.expected.md new file mode 100644 index 0000000000..09adacc650 --- /dev/null +++ b/apps/web/tests/snapshots/settings-chrome/plugin-controls.expected.md @@ -0,0 +1,22 @@ +- banner: + - heading "内置插件" [level=3] + - paragraph: 开关会保存到当前配置;重启 DSH 后装载或卸载对应插件。 +- list: + - listitem: + - strong: dsh-genui + - link "查看源码": + - /url: https://github.com/omdsh-dev/dsh-genui + - text: 已开启 + - switch "关闭 dsh-genui" [checked] + - listitem: + - strong: dsh-annotation + - link "查看源码": + - /url: https://github.com/omdsh-dev/dsh-annotation + - text: 已开启 + - switch "关闭 dsh-annotation" [checked] + - listitem: + - strong: dsh-web-ui + - link "查看源码": + - /url: https://github.com/zhu1090093659/dsh-web-ui + - text: 已开启 + - switch "关闭 dsh-web-ui" [checked] diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index f5a845ad5f..ee4acca19f 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 20919b3fdc5ab26255465949d72bdce8d356a529 -config-catalog.zh.md: 8dfb49df5e3f5906af859a4de83ebc44f315a0bd +config-catalog.md: dc6119b086f649a4eb0192e2cac814864ee476db +config-catalog.zh.md: e86fad9b9119c82b70b2971230f71bdd6538bc89 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 20919b3fdc..dc6119b086 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -785,6 +785,36 @@ export interface Config { Source: [`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts) + + +## `@deepseek-ai/dsh-host-plugin-control` + +Requires: `loader` · `connection` + +```ts config-catalog +/** Plugin-control gateway configuration owned by the composing profile. */ +export interface Config { + /** Absolute user patch layer of the running profile. */ + profilePatchPath: string + /** Logical products that this deployment permits the browser to control. */ + controls: PluginControlSpec[] +} + +/** One deployment-configured logical control and its Loader rows. */ +export interface PluginControlSpec { + /** Stable id used in the profile patch marker and mutation request. */ + id: string + /** Human-readable product name shown in Settings. */ + name: string + /** HTTP(S) source repository shown in Settings. */ + repository: string + /** Complete Loader entry-id set controlled as one product. */ + entryIds: string[] +} +``` + +Source: [`packages/host/plugin-control/src/index.ts:44`](../packages/host/plugin-control/src/index.ts) + ## `@deepseek-ai/dsh-host-webserver` @@ -3049,6 +3079,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@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)) - `@deepseek-ai/dsh-client-ui-settings-models` ([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-settings-plugin-control` ([`packages/client/ui-settings-plugin-control/src/index.ts`](../packages/client/ui-settings-plugin-control/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings-plugin-inventory` ([`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings-plugins` ([`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 8dfb49df5e..e86fad9b91 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -787,6 +787,36 @@ export interface Config { 来源:[`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts) + + +## `@deepseek-ai/dsh-host-plugin-control` + +需要:`loader` · `connection` + +```ts config-catalog +/** Plugin-control gateway configuration owned by the composing profile. */ +export interface Config { + /** Absolute user patch layer of the running profile. */ + profilePatchPath: string + /** Logical products that this deployment permits the browser to control. */ + controls: PluginControlSpec[] +} + +/** One deployment-configured logical control and its Loader rows. */ +export interface PluginControlSpec { + /** Stable id used in the profile patch marker and mutation request. */ + id: string + /** Human-readable product name shown in Settings. */ + name: string + /** HTTP(S) source repository shown in Settings. */ + repository: string + /** Complete Loader entry-id set controlled as one product. */ + entryIds: string[] +} +``` + +来源:[`packages/host/plugin-control/src/index.ts:44`](../packages/host/plugin-control/src/index.ts) + ## `@deepseek-ai/dsh-host-webserver` @@ -3051,6 +3081,7 @@ export interface Config { - `@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)) - `@deepseek-ai/dsh-client-ui-settings-models`([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-settings-plugin-control`([`packages/client/ui-settings-plugin-control/src/index.ts`](../packages/client/ui-settings-plugin-control/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings-plugin-inventory`([`packages/client/ui-settings-plugin-inventory/src/index.ts`](../packages/client/ui-settings-plugin-inventory/src/index.ts)) - `@deepseek-ai/dsh-client-ui-settings-plugins`([`packages/client/ui-settings-plugins/src/index.ts`](../packages/client/ui-settings-plugins/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar`([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts)) diff --git a/knip.json b/knip.json index 5ea64d5481..bb57dc7b6c 100644 --- a/knip.json +++ b/knip.json @@ -794,7 +794,20 @@ }, "packages/bundle/web-app": { "ignoreDependencies": [ - "@deepseek-ai/.+" + "@deepseek-ai/.+", + "@linxin666/dsh-client-ui-aionui-panel", + "@linxin666/dsh-client-ui-git-graph", + "@linxin666/dsh-client-ui-skin-center", + "@linxin666/dsh-client-ui-skin-whale-song", + "@linxin666/dsh-client-ui-task-board", + "@linxin666/dsh-client-ui-web-ui-settings", + "@linxin666/dsh-live-stats", + "@linxin666/dsh-pet", + "@linxin666/dsh-remote-web-ui", + "@linxin666/dsh-ssh", + "@linxin666/dsh-web-ui-all", + "@omdsh-dev/dsh-annotation", + "@omdsh-dev/dsh-genui" ] } } diff --git a/package.json b/package.json index 4f6294f6ed..b19ce13727 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "demo:acp": "node --import tsx packages/examples/acp-demo/src/bin.ts --config examples/acp-agent/cordis.yml", "mock:llm": "node --import tsx packages/test-support/llm-mock-server/src/bin.ts", "dev:web": "tsx scripts/dev-web.ts --poll", - "postinstall": "node scripts/install-lefthook.mjs" + "postinstall": "node scripts/install-lefthook.mjs && node scripts/link-community-skins.mjs" }, "devDependencies": { "@agentclientprotocol/sdk": "0.25.1", diff --git a/packages/api/remotes/README.i18n.yaml b/packages/api/remotes/README.i18n.yaml index b9ff0c0323..7b0156ce8e 100644 --- a/packages/api/remotes/README.i18n.yaml +++ b/packages/api/remotes/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/api/remotes/README.md -README.md: 18d39c6e86f114d2aac2f24e5d15c13335eab020 -README.zh.md: bf3dfbbaa6e0c7bd1da8398977837d4cb19d4688 +README.md: 7ac740736459db703280d90bda7a38a458680498 +README.zh.md: 6a3883f013eb28c32faf0cf9a8f5dae711be0a44 diff --git a/packages/api/remotes/README.md b/packages/api/remotes/README.md index 18d39c6e86..7ac7407364 100644 --- a/packages/api/remotes/README.md +++ b/packages/api/remotes/README.md @@ -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 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. +The current Client assembly mounts the Goal Remote contribution and the read-only Host plugin inventory contribution (`pluginInventory/list`). It also type-reexports the plugin-control wire DTOs for generic Connection callers; no plugin-control Typert Remote is mounted. Cordis effect ownership withdraws every mounted 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. diff --git a/packages/api/remotes/README.zh.md b/packages/api/remotes/README.zh.md index bf3dfbbaa6..6a3883f013 100644 --- a/packages/api/remotes/README.zh.md +++ b/packages/api/remotes/README.zh.md @@ -6,7 +6,7 @@ `createApiRemoteAgentResolver()` 会复用 live Agent、恢复普通冷会话、对并发恢复去重、保留 subagent ownership fence,并为 Typert `agent` 和 `session` lookup 配置同一个 resolver。标准 Web API Proxy 提供 Agent 默认值和 scope 设置,再将返回的 resolver 用于旧方法,使已迁移与未迁移方法共用同一份策略实现。 -当前 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 实现的边。 +当前 Client 组合挂载 Goal Remote 贡献和只读 Host 插件清单贡献(`pluginInventory/list`)。它还为使用通用 Connection 的调用方以 type-only 形式重新导出插件开关 wire DTO,但不挂载插件开关 Typert Remote。该组合卸载时,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。 diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 42a7d14070..7bfbbb2a7b 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -65,6 +65,7 @@ "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-cordis-host-runner": "workspace:^", + "@deepseek-ai/dsh-host-plugin-control": "workspace:^", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-agent-presets": "workspace:^", @@ -82,6 +83,7 @@ "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-cordis-host-runner": "workspace:^", + "@deepseek-ai/dsh-host-plugin-control": "workspace:^", "@deepseek-ai/dsh-host-plugin-inventory": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-agent-presets": "workspace:^", diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 210ec0bf80..5bb1bc9000 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -9,6 +9,13 @@ import messageFeedbackRemote from '@deepseek-ai/dsh-message-feedback/remote' import type { TypertClientRemote } from '@deepseek-ai/dsh-typert-protocol' export type { TypertClientRemote as ClientRemote } from '@deepseek-ai/dsh-typert-protocol' +export type { + PluginControlId, + PluginControlItem, + PluginControlSnapshot, + PluginControlState, + SetPluginEnabledRequest, +} from '@deepseek-ai/dsh-host-plugin-control/types' export type { PluginInventorySnapshot } from '@deepseek-ai/dsh-host-plugin-inventory/types' export type {} from '@deepseek-ai/dsh-commands/remote' export type {} from '@deepseek-ai/dsh-goal/remote' diff --git a/packages/api/remotes/tsconfig.client.json b/packages/api/remotes/tsconfig.client.json index a0161dd047..9684e17bc7 100644 --- a/packages/api/remotes/tsconfig.client.json +++ b/packages/api/remotes/tsconfig.client.json @@ -33,6 +33,9 @@ { "path": "../../feedback/message-feedback" }, + { + "path": "../../host/plugin-control" + }, { "path": "../../host/plugin-inventory" }, diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml index 81098dca30..66cba871d7 100644 --- a/packages/boot/app-boot/README.i18n.yaml +++ b/packages/boot/app-boot/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md -README.md: 80f8e8a694b32eb8a1499f75a56de852f7106643 -README.zh.md: c8396625f2f59008d71b865f35a04ba55bb313cd +README.md: 33fdf78f4cd68d20df77eadaec5a311a91a499e5 +README.zh.md: eed34df2d9e7f2342558d8cb3adcfa68653c1904 diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md index 80f8e8a694..33fdf78f4c 100644 --- a/packages/boot/app-boot/README.md +++ b/packages/boot/app-boot/README.md @@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](.. ## Profiles -A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; other names fail loud until `initProfile` creates them (the `dsh plugin` path). `loadProfile` normalizes an exact installation-owned bundle tuple to its shipped template while preserving every other manifest field; any extra, missing, or reordered entry makes the list user-owned and leaves it unchanged. +A profile is a directory under `$DSH_HOME/profiles/` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list — and the user's own `cordis.patch.yml`. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory — one symlink per package the installation's app and bundles depend on — so bare plugin names in any profile resolve through Node's ordinary parent-walk without pnpm managing in-box packages. `PROFILE_TEMPLATES` (`web`, `headless`) auto-initialize on first use; the Web template orders dsh-genui, dsh-annotation, and dsh-web-ui after the in-box base and Web layers. Other names fail loud until `initProfile` creates them (the `dsh plugin` path). `loadProfile` normalizes an exact installation-owned bundle tuple to its shipped template while preserving every other manifest field; the prior two-bundle Web tuple is installation-owned and migrates to the current five-bundle template, while any extra, missing, or reordered entry makes the list user-owned and leaves it unchanged. The profile launcher provides the absolute `profileUserPatchPath` context slot before rows mount, allowing a trusted Host plugin to update only that profile layer without inferring Harness-home paths. User-level machine-local preferences also live in the Harness home: diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md index c8396625f2..eed34df2d9 100644 --- a/packages/boot/app-boot/README.zh.md +++ b/packages/boot/app-boot/README.zh.md @@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面 ## Profiles -profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES`(`web`、`headless`)在首次使用时自动初始化;其他名称在 `initProfile` 创建之前都会明确报错(即 `dsh plugin` 路径)。`loadProfile` 会将与安装自有组合包元组完全一致的列表规范化为随发行版交付的模板,同时保留 manifest 中其他所有字段;一旦条目有任何额外、缺失或重排,该列表就归用户所有并保持不变。 +profile 是位于 `$DSH_HOME/profiles/` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和用户自己的 `cordis.patch.yml`。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录(安装目录的应用与各组合包依赖的每个包对应一个符号链接),使任意 profile 中的裸插件名都能经 Node 常规的逐级向上查找解析,而无需由 pnpm 管理随安装内置的包。`PROFILE_TEMPLATES`(`web`、`headless`)在首次使用时自动初始化;Web 模板会在内置 base 与 Web 层之后依次加入 dsh-genui、dsh-annotation 和 dsh-web-ui。其他名称在 `initProfile` 创建之前都会明确报错(即 `dsh plugin` 路径)。`loadProfile` 会将与安装自有组合包元组完全一致的列表规范化为随发行版交付的模板,同时保留 manifest 中其他所有字段;旧的双组合包 Web 元组归安装所有并会迁移到当前五组合包模板,一旦条目有任何额外、缺失或重排,该列表就归用户所有并保持不变。profile 启动器会在配置行挂载前提供绝对路径 `profileUserPatchPath` 上下文插槽,使受信任的 Host 插件只更新该 profile 层,而无需推断 Harness home 路径。 用户级的机器本地偏好同样位于 harness home 中: diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index 9be66947bb..33e1d05a8f 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -25,6 +25,8 @@ declare module '@deepseek-ai/cordis' { interface Context { /** Harness-home path resolver available to Loader `!!js` config expressions. */ dshHomePath?: typeof dshHomePath + /** Absolute user patch layer of the profile currently being launched. */ + profileUserPatchPath?: string } } diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 8f982bed80..c2d07ab85e 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -112,12 +112,19 @@ export function resolveProfileDir(name: string, home: string = resolveDshHome()) /** The shipped profile templates auto-initialized on first use, by name. */ export const PROFILE_TEMPLATES: Record = { - web: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'], + web: [ + '@deepseek-ai/dsh-base', + '@deepseek-ai/dsh-web-app', + '@omdsh-dev/dsh-genui', + '@omdsh-dev/dsh-annotation', + '@linxin666/dsh-web-ui-all', + ], headless: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-headless'], } /** Installation-owned bundle tuples normalized to the shipped template. */ const INSTALLATION_OWNED_PROFILE_TUPLES: Record = { + web: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'], headless: ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-headless'], } diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index bd0294475d..9ff09a32a6 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -152,6 +152,13 @@ describe('loadProfile', () => { // cannot be asserted to fail here: the source-plane test runner resolves // @deepseek-ai/* through tsconfig paths regardless of the staged anchor. expect(PROFILE_TEMPLATES.web).toContain('@deepseek-ai/dsh-base') + expect(PROFILE_TEMPLATES.web).toEqual([ + '@deepseek-ai/dsh-base', + '@deepseek-ai/dsh-web-app', + '@omdsh-dev/dsh-genui', + '@omdsh-dev/dsh-annotation', + '@linxin666/dsh-web-ui-all', + ]) try { loadProfile('t', 'web', anchor, home) } catch { @@ -161,6 +168,30 @@ describe('loadProfile', () => { .toEqual([...PROFILE_TEMPLATES.web ?? []]) }) + it('normalizes only the former installation-owned web bundle tuple', () => { + const anchor = stageInstallation({ + '@deepseek-ai/dsh-base': { patch: '[]\n' }, + '@deepseek-ai/dsh-web-app': { patch: '[]\n' }, + '@omdsh-dev/dsh-genui': { patch: '[]\n' }, + '@omdsh-dev/dsh-annotation': { patch: '[]\n' }, + '@linxin666/dsh-web-ui-all': { patch: '[]\n' }, + 'custom-bundle': { patch: '[]\n' }, + }) + const home = tmp() + const stock = resolveProfileDir('web', home) + initProfile(stock, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + loadProfile('t', 'web', anchor, home) + expect(readProfileManifest('t', stock).dsh?.profile?.bundles).toEqual(PROFILE_TEMPLATES.web) + + const customHome = tmp() + const custom = resolveProfileDir('web', customHome) + initProfile(custom, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'custom-bundle']) + loadProfile('t', 'web', anchor, customHome) + expect(readProfileManifest('t', custom).dsh?.profile?.bundles).toEqual([ + '@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', 'custom-bundle', + ]) + }) + it('normalizes only the exact installation-owned headless bundle tuple', () => { const anchor = stageInstallation({ '@deepseek-ai/dsh-base': { patch: '[]\n' }, diff --git a/packages/bundle/web-app/README.i18n.yaml b/packages/bundle/web-app/README.i18n.yaml index 220b4d78ff..9b9143acb2 100644 --- a/packages/bundle/web-app/README.i18n.yaml +++ b/packages/bundle/web-app/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md -README.md: 90d1566b5a7a25f6a827079c4a8ab776e05cc7a3 -README.zh.md: b7156bcab66b53964bb2f0e1c87d9e9a806e3f75 +README.md: 3b6d90ccade73efb017b325665ff637da0e41201 +README.zh.md: f92915a0ffa12a0a01517864c4c3f054ea1aa2c4 diff --git a/packages/bundle/web-app/README.md b/packages/bundle/web-app/README.md index 90d1566b5a..3b6d90ccad 100644 --- a/packages/bundle/web-app/README.md +++ b/packages/bundle/web-app/README.md @@ -4,6 +4,10 @@ English | [中文](README.zh.md) The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. It rejects `--host 0.0.0.0` before publishing that service because the CLI intentionally does not support all-interfaces binding yet. Flag-configured rows inject the service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle. +The shipped `web` profile layers [dsh-genui](https://github.com/omdsh-dev/dsh-genui), [dsh-annotation](https://github.com/omdsh-dev/dsh-annotation), and the [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) aggregate after this bundle, so all three community products are present and enabled by default. This bundle's manifest declares the aggregate's nine entry packages plus the whale-song skin package as direct dependencies at the same pinned version so the profile module fallback can resolve their entry rows from a profile directory; it also contributes the loopback-only [`plugin-control`](../../host/plugin-control/README.md) Host row and the browser [`Plugin switches`](../../client/ui-settings-plugin-control/README.md) tab. The three cards persist profile-local enablement for GenUI, Annotation, and all nine dsh-web-ui rows as one product; changes take effect after DSH restarts. The read-only Loader inventory remains a separate tab and no arbitrary inventory row becomes mutable. + +The upstream skin center reads a `skins/` directory beside its own location, which no bundled deployment provides; the repo patches `@linxin666/dsh-client-ui-skin-center` (`patchedDependencies`) to also walk ancestor directories, to always insert the active skin's row into the managed patch section (the published aggregate wires no skin rows), and to reconcile the running Loader tree live after an apply — required because packaged Electron cannot provide Cordis HMR and the desktop does not watch patch files. `scripts/link-community-skins.mjs` (postinstall) plus the desktop packaging stage assemble the installed skin packages into that tree, so all seven skin-center cards (including whale-song) work in source and packaged deployments. + ## Model Experience ### Harness-source and Web-surface context @@ -20,7 +24,22 @@ One source line and one prompt paragraph per session plus two managed-environmen The prompt section sits near the system prompt's head and is stable for the life of the process (the port is a boot fact), so it does not invalidate the cache across turns. +### Built-in community plugins + +#### What the model sees + +With the shipped defaults, dsh-genui adds its `dsh-ui` output instructions and the `render_ui` and validation tools; dsh-annotation adds model-visible annotation text only when the user sends annotations; dsh-web-ui includes the SSH tool and prompt contribution alongside its browser panels, task board, Git graph, pet, live statistics, remote Web UI, settings, and skins. The upstream packages own their detailed prompts, tools, persistence, remote-access controls, and security behavior. In particular, SSH host configuration and credentials remain host data, SSH routes are loopback-only, and remote Web access requires the plugin's pairing flow. + +#### Token effect + +GenUI and SSH add their fixed instructions and tool schemas while enabled; Annotation adds text only to requests that carry a user annotation. Browser-only panels add no model tokens. + +#### KV Cache effect + +The GenUI and SSH prompt/tool contributions are stable within a process. A Plugin switch can change the next process's request prefix and tool list, which starts a new provider cache prefix after restart. + ## Known Limitations and Deferred Work - **The frontend dist must be built** — `require.resolve` of the dist fails loud at activation with a build hint; there is no source-serving fallback. - **`lanAddresses` is a boot-time snapshot** — interface changes after boot are not re-advertised; the printed LAN URL always matches the configured trust fence. +- **Community switches require restart** — external plugins are not assumed to release every route, tool, or browser registration safely during live teardown, so Settings persists the desired profile state without mutating the running tree. diff --git a/packages/bundle/web-app/README.zh.md b/packages/bundle/web-app/README.zh.md index b7156bcab6..f92915a0ff 100644 --- a/packages/bundle/web-app/README.zh.md +++ b/packages/bundle/web-app/README.zh.md @@ -4,6 +4,10 @@ dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host`、`--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。它会在发布该服务前拒绝 `--host 0.0.0.0`,因为 CLI 目前有意不支持绑定所有网络接口。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。 +随发行版提供的 `web` profile 会在本组合包之后叠加 [dsh-genui](https://github.com/omdsh-dev/dsh-genui)、[dsh-annotation](https://github.com/omdsh-dev/dsh-annotation) 与 [dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui) 聚合包,因此三个社区产品都默认存在并启用。本组合包的 manifest 把聚合包的九个入口包与 whale-song 皮肤包声明为同一锁定版本的直接依赖,使 profile 模块回退目录能从 profile 目录解析这些入口行;它还贡献仅限回环访问的 [`plugin-control`](../../host/plugin-control/README.md) Host 行和浏览器端[插件开关](../../client/ui-settings-plugin-control/README.md)标签页。三张卡片分别持久化 GenUI、Annotation,以及作为一个产品统一控制的九个 dsh-web-ui 行;更改在 DSH 重启后生效。只读 Loader 清单仍是独立标签页,任意清单行不会因此变成可修改项。 + +上游皮肤中心会在自身所在位置旁边读取 `skins/` 目录,而任何打包部署都不提供该目录;本仓库通过 `patchedDependencies` 给 `@linxin666/dsh-client-ui-skin-center` 打补丁:让它在原始位置不存在时沿祖先目录查找、让受管区段总是插入当前皮肤的插入行(已发布的聚合包没有把任何皮肤行装进组合层),并在应用成功后对运行中的 Loader 树做 live reconcile——打包 Electron 无法提供 Cordis HMR 且桌面版不监听 patch 文件,这一步是应用即时生效的唯一通道。`scripts/link-community-skins.mjs`(postinstall)与桌面打包的 stage 步骤把已安装的皮肤包装进这棵树,因此皮肤中心的七张卡片(含 whale-song)在源码启动与打包部署下都能试用和应用。 + ## 模型体验 ### Harness 源码与 Web 表层上下文 @@ -20,7 +24,22 @@ dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 该提示词段落位于系统提示词靠前位置,且在进程整个生命周期内稳定(端口是启动期事实),因此不会使跨轮次缓存失效。 +### 内置社区插件 + +#### 模型看到的内容 + +按随发行版提供的默认值,dsh-genui 会添加 `dsh-ui` 输出指令以及 `render_ui` 和校验工具;dsh-annotation 只在用户发送批注时加入模型可见的批注文本;dsh-web-ui 除浏览器面板、任务看板、Git 图、宠物、实时统计、远程 Web UI、设置与皮肤外,还包含 SSH 工具和提示词贡献。上游包拥有其详细提示词、工具、持久化、远程访问控制与安全行为。具体而言,SSH 主机配置和凭据仍属于宿主数据,SSH 路由仅限回环访问,远程 Web 访问则需要使用该插件的配对流程。 + +#### Token 影响 + +GenUI 与 SSH 在启用时加入各自固定的指令和工具 schema;Annotation 只在请求携带用户批注时加入文本。仅用于浏览器的面板不会增加模型 token。 + +#### KV Cache 影响 + +GenUI 与 SSH 的提示词和工具贡献在单个进程内保持稳定。插件开关可以改变下一个进程的请求前缀和工具列表,因此重启后会开始使用新的提供方缓存前缀。 + ## 已知限制与延期工作 - **前端 dist 必须已构建**:对 dist 的 `require.resolve` 在激活时明确报错并给出构建提示;没有从源码直接服务的回退路径。 - **`lanAddresses` 是启动期快照**:启动后的网卡变化不会重新公告;打印的 LAN URL 始终与配置的信任栅栏一致。 +- **社区插件开关需要重启**:不会假定外部插件能在实时 teardown 时安全释放全部路由、工具或浏览器注册,因此 Settings 只持久化期望的 profile 状态,不修改运行中的树。 diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index b06e63b54f..9d93734eea 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -94,6 +94,36 @@ - id: plugin-inventory name: '@deepseek-ai/dsh-host-plugin-inventory' + # Loopback-only profile controls. The catalog groups the requested + # community bundles as three products; dsh-web-ui's rows move together. + - id: plugin-control + name: '@deepseek-ai/dsh-host-plugin-control' + inject: [profileUserPatchPath] + config: + profilePatchPath: !!js ctx.profileUserPatchPath + controls: + - id: genui + name: dsh-genui + repository: https://github.com/omdsh-dev/dsh-genui + entryIds: [genui] + - id: annotation + name: dsh-annotation + repository: https://github.com/omdsh-dev/dsh-annotation + entryIds: [dsh-annotation] + - id: web-ui + name: dsh-web-ui + repository: https://github.com/zhu1090093659/dsh-web-ui + entryIds: + - ui-dsh-aionui-panel + - ui-task-board + - ui-git-graph + - pet + - remote-web-ui + - live-stats + - ssh + - ui-web-ui-settings + - ui-skin-center + # 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 @@ -195,6 +225,9 @@ - id: ui-settings-plugin-inventory name: '@deepseek-ai/dsh-client-ui-settings-plugin-inventory' + - id: ui-settings-plugin-control + name: '@deepseek-ai/dsh-client-ui-settings-plugin-control' + - id: ui-conversation name: '@deepseek-ai/dsh-client-ui-conversation' diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index ca4709a2a9..607560e244 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -59,63 +59,78 @@ "@deepseek-ai/dsh-client-ui-deliverables": "workspace:^", "@deepseek-ai/dsh-client-ui-directory-picker-browse": "workspace:^", "@deepseek-ai/dsh-client-ui-directory-picker-native": "workspace:^", - "@deepseek-ai/dsh-client-ui-message-feedback": "workspace:^", "@deepseek-ai/dsh-client-ui-goal": "workspace:^", + "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", + "@deepseek-ai/dsh-client-ui-jobs": "workspace:^", "@deepseek-ai/dsh-client-ui-layout": "workspace:^", + "@deepseek-ai/dsh-client-ui-message-feedback": "workspace:^", "@deepseek-ai/dsh-client-ui-model-selection": "workspace:^", - "@deepseek-ai/dsh-client-ui-settings-models": "workspace:^", - "@deepseek-ai/dsh-client-ui-settings-plugin-inventory": "workspace:^", "@deepseek-ai/dsh-client-ui-permission-presets": "workspace:^", "@deepseek-ai/dsh-client-ui-plan": "workspace:^", - "@deepseek-ai/dsh-client-ui-settings-plugins": "workspace:^", - "@deepseek-ai/dsh-client-ui-user-questions": "workspace:^", "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-settings-general": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings-models": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings-plugin-control": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings-plugin-inventory": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings-plugins": "workspace:^", "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", "@deepseek-ai/dsh-client-ui-skill": "workspace:^", - "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^", "@deepseek-ai/dsh-client-ui-subagent": "workspace:^", - "@deepseek-ai/dsh-client-ui-jobs": "workspace:^", "@deepseek-ai/dsh-client-ui-theme": "workspace:^", "@deepseek-ai/dsh-client-ui-tool": "workspace:^", - "@deepseek-ai/dsh-client-ui-workflow-run": "workspace:^", "@deepseek-ai/dsh-client-ui-trajectory": "workspace:^", + "@deepseek-ai/dsh-client-ui-user-questions": "workspace:^", + "@deepseek-ai/dsh-client-ui-workflow-run": "workspace:^", "@deepseek-ai/dsh-client-ui-workspace": "workspace:^", "@deepseek-ai/dsh-cmdline": "workspace:^", "@deepseek-ai/dsh-code-runtime-worker-thread": "workspace:^", "@deepseek-ai/dsh-cordis-client-runner": "workspace:^", "@deepseek-ai/dsh-cordis-host-runner": "workspace:^", - "@deepseek-ai/dsh-web-frontend": "workspace:^", - "@deepseek-ai/dsh-host-frontend-static": "workspace:^", "@deepseek-ai/dsh-host-apiproxy": "workspace:^", "@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-frontend-static": "workspace:^", + "@deepseek-ai/dsh-host-plugin-control": "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:^", "@deepseek-ai/dsh-session-log-export": "workspace:^", + "@deepseek-ai/dsh-session-projection-cache": "workspace:^", "@deepseek-ai/dsh-session-stats": "workspace:^", "@deepseek-ai/dsh-storage": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", "@deepseek-ai/dsh-storage-json": "workspace:^", + "@deepseek-ai/dsh-web-frontend": "workspace:^", "@deepseek-ai/dsh-workspace": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", + "@linxin666/dsh-client-ui-aionui-panel": "0.1.2", + "@linxin666/dsh-client-ui-git-graph": "0.1.2", + "@linxin666/dsh-client-ui-skin-center": "0.1.2", + "@linxin666/dsh-client-ui-skin-whale-song": "0.1.2", + "@linxin666/dsh-client-ui-task-board": "0.1.2", + "@linxin666/dsh-client-ui-web-ui-settings": "0.1.2", + "@linxin666/dsh-live-stats": "0.1.2", + "@linxin666/dsh-pet": "0.1.2", + "@linxin666/dsh-remote-web-ui": "0.1.2", + "@linxin666/dsh-ssh": "0.1.2", + "@linxin666/dsh-web-ui-all": "0.1.2", + "@omdsh-dev/dsh-annotation": "github:omdsh-dev/dsh-annotation#a37edb01c3dbb0b19249cab3b93138ad529281ed", + "@omdsh-dev/dsh-genui": "github:omdsh-dev/dsh-genui#57b4338222632f8ea81c2665d44e5f9e80b52686", "commander": "^15.0.0" }, "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/cordis-plugin-loader": "workspace:^", - "@deepseek-ai/dsh-shell-env": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/dsh-shell-env": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^" }, "devDependencies": { + "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/cordis-plugin-loader": "workspace:^", - "@deepseek-ai/dsh-shell-env": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/dsh-system-prompt": "workspace:^", - "@deepseek-ai/cordis": "workspace:^" + "@deepseek-ai/dsh-shell-env": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^" } } diff --git a/packages/client/ui-settings-plugin-control/README.i18n.yaml b/packages/client/ui-settings-plugin-control/README.i18n.yaml new file mode 100644 index 0000000000..818ef26234 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/README.i18n.yaml @@ -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-settings-plugin-control/README.md +README.md: e4937f4a40bb2313621afac0db4741db0ca8a7e0 +README.zh.md: e17251f8824d03984b085d01d2e59492957dd80c diff --git a/packages/client/ui-settings-plugin-control/README.md b/packages/client/ui-settings-plugin-control/README.md new file mode 100644 index 0000000000..e4937f4a40 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/README.md @@ -0,0 +1,31 @@ +# @deepseek-ai/dsh-client-ui-settings-plugin-control + +English | [中文](README.zh.md) + +The **Plugin switches** tab for Web Settings. The browser plugin contributes id `controls` to the root-scoped `settings.plugins.tab` list and registers its own English and Simplified Chinese copy. It performs no privileged request during activation; selecting the tab mounts the component and lazily reads the configured logical products through the generic Connection channel `/plugin-control`. + +Each product card shows its deployment-provided name, upstream repository link, saved aggregate state, and an accessible `role="switch"` control. A mutation disables the other switches while pending, replaces the local snapshot with the Host response, and displays a restart notice after the setting is saved. Loading, empty, unavailable, retry, and generic failure states do not expose transport or filesystem details. The registration uses `ctx.slots.inject()`, so it follows late tab declaration, redeclaration, locale changes, and teardown without importing the Plugins section owner. + +Remote browsers receive a local-only notice and never call the control route. The Host applies the same loopback trust decision at the route, so hiding the controls is presentation rather than the security check. + +## Model Experience + +### Browser plugin selection + +#### What the model sees + +Nothing from `Plugin switches`. It renders and mutates deployment settings in the browser and registers no model-facing content; after restart, a controlled plugin may contribute its own content. + +#### Token effect + +Zero in the current process. Any post-restart token change belongs to the controlled plugin. + +#### KV Cache effect + +None in the current process; any post-restart effect belongs to the enabled or disabled plugin. + +## Known Limitations and Deferred Work + +- **Restart required** — a successful switch updates desired state and the profile patch but deliberately leaves the running plugin mounted or unmounted. +- **One snapshot per mount, retry, or mutation** — the tab has no Loader or filesystem subscription; switching tabs preserves the current component state. +- **Deployment-defined grouping** — one card may govern several Loader rows, and a missing or duplicated row makes that entire card unavailable. diff --git a/packages/client/ui-settings-plugin-control/README.zh.md b/packages/client/ui-settings-plugin-control/README.zh.md new file mode 100644 index 0000000000..e17251f882 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/README.zh.md @@ -0,0 +1,31 @@ +# @deepseek-ai/dsh-client-ui-settings-plugin-control + +[English](README.md) | 中文 + +Web 设置中的**插件开关**标签页。浏览器插件向根级 `settings.plugins.tab` 列表贡献 id 为 `controls` 的条目,并注册自己的英文和简体中文文案。插件激活期间不会发出特权请求;选择该标签页后才挂载组件,并通过通用 Connection 通道 `/plugin-control` 懒读取配置好的逻辑产品。 + +每张产品卡片显示部署方提供的名称、上游仓库链接、已保存的聚合状态,以及一个可访问的 `role="switch"` 控件。修改进行期间会停用其他开关,以 Host 响应替换本地快照,并在设置保存后显示重启提示。加载、空结果、不可用、重试与通用失败状态都不会暴露传输或文件系统细节。注册使用 `ctx.slots.inject()`,因此能跟随标签 slot 的延迟声明、重新声明、本地化变化与 teardown,而无需 import “插件”分区拥有方。 + +远程浏览器只会看到“仅限本机”的提示,且不会调用控制路由。Host 会在路由处执行相同的回环信任判断,因此隐藏控件只是展示行为,并非安全检查本身。 + +## 模型体验 + +### 浏览器插件选择 + +#### 模型看到的内容 + +`插件开关` 自身没有任何内容。它在浏览器中显示和修改部署设置,不注册面向模型的内容;重启后,受控插件可能加入自己的内容。 + +#### Token 影响 + +当前进程中为零。重启后的 token 变化属于受控插件。 + +#### KV Cache 影响 + +对当前进程没有影响;重启后的任何影响归启用或停用的插件所有。 + +## 已知限制与暂缓事项 + +- **需要重启** —— 开关成功后会更新期望状态和 profile patch,但刻意不改变当前插件的挂载状态。 +- **每次挂载、重试或修改读取一份快照** —— 标签页不订阅 Loader 或文件系统变化;切换标签页会保留当前组件状态。 +- **由部署方定义分组** —— 一张卡片可以控制多个 Loader 行;任一行缺失或重复都会使整张卡片不可用。 diff --git a/packages/client/ui-settings-plugin-control/package.json b/packages/client/ui-settings-plugin-control/package.json new file mode 100644 index 0000000000..53190dfd6d --- /dev/null +++ b/packages/client/ui-settings-plugin-control/package.json @@ -0,0 +1,82 @@ +{ + "name": "@deepseek-ai/dsh-client-ui-settings-plugin-control", + "description": "Loopback-only plugin switches tab in Web Plugins settings", + "version": "0.1.0-rc.5", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/client/ui-settings-plugin-control" + }, + "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-client-connection", + "@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": "MIT", + "peerDependencies": { + "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-connection": "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-connection": "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" + ] +} diff --git a/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.module.css b/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.module.css new file mode 100644 index 0000000000..882d99b0af --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.module.css @@ -0,0 +1,225 @@ +.section { + display: flex; + flex-direction: column; + gap: 14px; + width: 100%; + max-width: 760px; + color: var(--dsw-alias-label-primary); +} + +.heading h3, +.heading p, +.status, +.failure p, +.notice p, +.mutationFailure, +.applying, +.saved { + margin: 0; +} + +.heading { + display: flex; + flex-direction: column; + gap: 4px; +} + +.heading h3 { + font-size: 14px; + line-height: 22px; + font-weight: 600; +} + +.heading p, +.status, +.applying, +.saved { + color: var(--dsw-alias-label-tertiary); + font-size: 13px; + line-height: 20px; +} + +.cards { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} + +.card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + min-height: 64px; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 10px; + padding: 11px 14px; + background: var(--dsw-alias-bg-layer-3); +} + +.product { + display: flex; + min-width: 0; + flex-direction: column; + gap: 2px; +} + +.product strong { + overflow: hidden; + font-size: 14px; + line-height: 20px; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.product a { + width: fit-content; + color: var(--dsw-alias-state-business-primary); + font-size: 12px; + line-height: 18px; + text-decoration: none; +} + +.product a:hover { + text-decoration: underline; +} + +.product a:focus-visible, +.failure button:focus-visible, +.switch:focus-visible { + outline: 2px solid var(--dsw-alias-state-business-primary); + outline-offset: 2px; +} + +.control { + display: flex; + flex: none; + align-items: center; + gap: 10px; +} + +.state { + color: var(--dsw-alias-label-tertiary); + font-size: 12px; + line-height: 18px; + white-space: nowrap; +} + +.state[data-state='enabled'] { + color: var(--dsw-alias-state-success-primary); +} + +.state[data-state='mixed'] { + color: var(--dsw-alias-state-warning-primary); +} + +.state[data-state='unavailable'] { + color: var(--dsw-alias-state-error-primary); +} + +.switch { + position: relative; + width: 34px; + height: 20px; + flex: none; + border: 0; + border-radius: 999px; + padding: 0; + background: var(--dsw-alias-border-l1); + cursor: pointer; +} + +.switch[aria-checked='true'] { + background: var(--dsw-alias-state-business-primary); +} + +.switch:disabled { + cursor: not-allowed; + opacity: 0.46; +} + +.switch span { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--dsw-alias-bg-layer-1); + box-shadow: var(--dsw-shadow-lv1); +} + +.switch[aria-checked='true'] span { + transform: translateX(14px); +} + +.failure { + display: flex; + align-items: center; + gap: 10px; + color: var(--dsw-alias-state-error-primary); + font-size: 13px; + line-height: 20px; +} + +.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; +} + +.mutationFailure { + color: var(--dsw-alias-state-error-primary); + font-size: 13px; + line-height: 20px; +} + +.notice { + max-width: 620px; + border: 1px solid var(--dsw-alias-border-l2); + border-radius: 10px; + padding: 14px 16px; + background: var(--dsw-alias-bg-layer-3); + color: var(--dsw-alias-label-primary); +} + +.notice strong { + font-size: 14px; + line-height: 22px; +} + +.notice p { + margin-top: 3px; + color: var(--dsw-alias-label-tertiary); + font-size: 13px; + line-height: 20px; +} + +@media (prefers-reduced-motion: no-preference) { + .switch, + .switch span { + transition: background-color 140ms var(--ds-ease-in-out), transform 140ms var(--ds-ease-in-out); + } +} + +@media (max-width: 520px) { + .card { + align-items: flex-start; + } + + .control { + padding-top: 1px; + } + + .state { + display: none; + } +} diff --git a/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.tsx b/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.tsx new file mode 100644 index 0000000000..be39125127 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/client/PluginControlSettingsTab.tsx @@ -0,0 +1,159 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react' +import type { + PluginControlId, + PluginControlSnapshot, +} from '@deepseek-ai/dsh-api-remotes/client' +import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type { PluginControlLocaleKey } from './locales.ts' +import css from './PluginControlSettingsTab.module.css' + +/** Registration-side transport face used by the control tab. */ +export interface PluginControlSettingsTabInjected { + /** Whether this browser has loopback authority to use the control route. */ + isLoopback: boolean + /** Read the current configured control catalog. */ + list: () => Promise + /** Persist one control's next-start state and return the updated catalog. */ + setEnabled: (pluginId: PluginControlId, enabled: boolean) => Promise +} + +/** Full component props assembled by the Settings slot renderer. */ +export type PluginControlSettingsTabProps = + PropsRuntime<'settings.plugins.tab'> + & PropsLocale<'settings.pluginControl'> + & InjectFace + +type ViewState = + | { readonly status: 'loading' } + | { readonly status: 'error' } + | { readonly status: 'ready'; readonly snapshot: PluginControlSnapshot } + +const STATE_KEYS = { + enabled: 'enabled', + disabled: 'disabled', + mixed: 'mixed', + unavailable: 'unavailable', +} satisfies Record + +/** Render configured logical products as accessible profile switches. */ +export function PluginControlSettingsTab({ + isLoopback, + list, + setEnabled, + t, +}: PluginControlSettingsTabProps): ReactNode { + const mounted = useRef(true) + const [request, setRequest] = useState(0) + const [pending, setPending] = useState(null) + const [mutationFailed, setMutationFailed] = useState(false) + const [saved, setSaved] = useState(false) + const [state, setState] = useState({ status: 'loading' }) + + useEffect(() => () => { mounted.current = false }, []) + + /* jscpd:ignore-start -- This request lifecycle is intentionally local: extracting it would couple independent Settings tabs. */ + useEffect(() => { + if (!isLoopback) return + let current = true + void Promise.resolve().then(() => list()).then( + (snapshot) => { if (current) setState({ status: 'ready', snapshot }) }, + () => { if (current) setState({ status: 'error' }) }, + ) + return () => { current = false } + }, [isLoopback, list, request]) + /* jscpd:ignore-end */ + + const retry = (): void => { + setMutationFailed(false) + setSaved(false) + setState({ status: 'loading' }) + setRequest(value => value + 1) + } + + const change = (pluginId: PluginControlId, enabled: boolean): void => { + setPending(pluginId) + setMutationFailed(false) + setSaved(false) + void Promise.resolve().then(() => setEnabled(pluginId, enabled)).then( + (snapshot) => { + if (!mounted.current) return + setState({ status: 'ready', snapshot }) + setPending(null) + setSaved(true) + }, + () => { + if (!mounted.current) return + setMutationFailed(true) + setPending(null) + }, + ) + } + + if (!isLoopback) { + return ( +
+ {t('localOnlyTitle')} +

{t('localOnlyBody')}

+
+ ) + } + + return ( +
+
+

{t('heading')}

+

{t('description')}

+
+ {/* jscpd:ignore-start -- Loading/retry markup follows the Settings vocabulary without creating a shared owner. */} + {state.status === 'loading' ?

{t('loading')}

: null} + {state.status === 'error' ? ( +
+

{t('error')}

+ +
+ ) : null} + {/* jscpd:ignore-end */} + {mutationFailed ?

{t('updateError')}

: null} + {pending !== null ?

{t('applying')}

: null} + {saved ?

{t('restartHint')}

: null} + {state.status === 'ready' && state.snapshot.controls.length === 0 + ?

{t('empty')}

+ : null} + {state.status === 'ready' && state.snapshot.controls.length > 0 ? ( +
    + {state.snapshot.controls.map((control) => { + const checked = control.state === 'enabled' + const unavailable = control.state === 'unavailable' + const switchLabel = t(checked ? 'disableSwitch' : 'enableSwitch', { name: control.name }) + return ( +
  • +
    + {control.name} + {t('source')} +
    +
    + {t(STATE_KEYS[control.state])} + +
    +
  • + ) + })} +
+ ) : null} +
+ ) +} diff --git a/packages/client/ui-settings-plugin-control/src/client/index.ts b/packages/client/ui-settings-plugin-control/src/client/index.ts new file mode 100644 index 0000000000..8dfd886d7f --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/client/index.ts @@ -0,0 +1,58 @@ +/** Loopback-only plugin controls registered into Web Settings. */ + +import type {} from '@deepseek-ai/dsh-client-locale/client' +import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' +import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' +import type { + PluginControlId, + PluginControlSnapshot, +} from '@deepseek-ai/dsh-api-remotes/client' +import { PluginControlSettingsTab, type PluginControlSettingsTabInjected } from './PluginControlSettingsTab.tsx' +import { en, zh, type PluginControlLocaleKey } from './locales.ts' +import { parsePluginControlSnapshot } from './protocol.ts' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** Copy for configured profile plugin controls. */ + 'settings.pluginControl': PluginControlLocaleKey + } +} + +const NS = 'settings.pluginControl' +const CHANNEL = '/plugin-control' + +/** Services required by the Settings registration and loopback RPC caller. */ +export const inject = ['slots', 'locale', 'connection'] + +/** Contribute the lazy plugin-control tab to the Plugins settings section. */ +export function apply(ctx: ClientContext): void { + ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-plugin-control: dictionaries') + + const connection = ctx.get('connection') as ConnectionHandle + const t = ctx.locale.bind(NS) + const call = async (endpoint: string, payload: unknown): Promise => { + const result = await connection.rpc.call(CHANNEL, endpoint, payload) + if (!result.ok) { + throw new Error(`plugin-control ${endpoint} failed: ${result.error.code}: ${result.error.message}`) + } + return parsePluginControlSnapshot(result.value) + } + const list = (): Promise => call('list', {}) + const setEnabled = (pluginId: PluginControlId, enabled: boolean): Promise => + call('set-enabled', { pluginId, enabled }) + const injected = (): PluginControlSettingsTabInjected => ({ + isLoopback: connection.isLoopback, + list, + setEnabled, + }) + + ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ + name: 'settings.plugins.tab', + id: 'controls', + order: 20, + label: () => t('tab'), + locale: NS, + inject: injected, + }, PluginControlSettingsTab)) +} diff --git a/packages/client/ui-settings-plugin-control/src/client/locales.ts b/packages/client/ui-settings-plugin-control/src/client/locales.ts new file mode 100644 index 0000000000..3cff8dd411 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/client/locales.ts @@ -0,0 +1,50 @@ +/** Copy dictionaries for the plugin-control Settings tab. */ + +/** Simplified Chinese dictionary and key source of truth. */ +export const zh = { + tab: '插件开关', + heading: '内置插件', + description: '开关会保存到当前配置;重启 DSH 后装载或卸载对应插件。', + loading: '正在读取插件状态…', + error: '暂时无法读取插件开关。', + retry: '重试', + empty: '当前配置没有可控制的插件。', + localOnlyTitle: '仅限本机操作', + localOnlyBody: '为了保护主机配置,插件开关只能从本机打开。', + source: '查看源码', + enabled: '已开启', + disabled: '已关闭', + mixed: '部分开启', + unavailable: '不可用', + enableSwitch: '开启 {name}', + disableSwitch: '关闭 {name}', + applying: '正在应用更改…', + restartHint: '更改已保存,请重启 DSH 使其生效。', + updateError: '更改未能应用,请重试。', +} satisfies Record + +/** Plugin-control locale key union. */ +export type PluginControlLocaleKey = keyof typeof zh + +/** English dictionary checked against the Chinese key set. */ +export const en = { + tab: 'Plugin switches', + heading: 'Built-in plugins', + description: 'Switches are saved to the current profile; restart DSH to mount or unmount the corresponding plugins.', + loading: 'Reading plugin states…', + error: 'Plugin switches are temporarily unavailable.', + retry: 'Retry', + empty: 'This profile has no controllable plugins.', + localOnlyTitle: 'Available on this computer only', + localOnlyBody: 'To protect host configuration, plugin switches can only be changed from a local browser.', + source: 'View source', + enabled: 'On', + disabled: 'Off', + mixed: 'Partially on', + unavailable: 'Unavailable', + enableSwitch: 'Turn on {name}', + disableSwitch: 'Turn off {name}', + applying: 'Applying change…', + restartHint: 'The change is saved. Restart DSH for it to take effect.', + updateError: 'The change could not be applied. Try again.', +} satisfies Record diff --git a/packages/client/ui-settings-plugin-control/src/client/protocol.ts b/packages/client/ui-settings-plugin-control/src/client/protocol.ts new file mode 100644 index 0000000000..b77cc68073 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/client/protocol.ts @@ -0,0 +1,40 @@ +import type { + PluginControlId, + PluginControlSnapshot, + PluginControlState, +} from '@deepseek-ai/dsh-api-remotes/client' + +const CONTROL_STATES = new Set(['enabled', 'disabled', 'mixed', 'unavailable']) + +/** Whether a wire value is a non-array object. */ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Validate the business payload returned through the generic Connection RPC. + * @param value - decoded but untrusted response value. + * @returns typed control snapshot. + */ +export function parsePluginControlSnapshot(value: unknown): PluginControlSnapshot { + if (!isRecord(value) || !Array.isArray(value.controls)) { + throw new Error('plugin-control: response must contain a controls array') + } + const controls = value.controls.map((item) => { + if (!isRecord(item) + || typeof item.id !== 'string' + || typeof item.name !== 'string' + || typeof item.repository !== 'string' + || typeof item.state !== 'string' + || !CONTROL_STATES.has(item.state as PluginControlState)) { + throw new Error('plugin-control: response contains an invalid control') + } + return { + id: item.id as PluginControlId, + name: item.name, + repository: item.repository, + state: item.state as PluginControlState, + } + }) + return { controls } +} diff --git a/packages/client/ui-settings-plugin-control/src/css-modules.d.ts b/packages/client/ui-settings-plugin-control/src/css-modules.d.ts new file mode 100644 index 0000000000..bc5e482353 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/css-modules.d.ts @@ -0,0 +1,6 @@ +declare module '*.module.css' { + const classes: Record + export default classes +} + +declare module '*.css' diff --git a/packages/client/ui-settings-plugin-control/src/index.ts b/packages/client/ui-settings-plugin-control/src/index.ts new file mode 100644 index 0000000000..ae106d72ed --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/index.ts @@ -0,0 +1,4 @@ +/** Host loader entry for the plugin-control browser implementation exported from `./client`. */ + +/** Host plugin body — no host-side behavior for the plugin-control tab. */ +export function apply(): void {} diff --git a/packages/client/ui-settings-plugin-control/src/invariant.ts b/packages/client/ui-settings-plugin-control/src/invariant.ts new file mode 100644 index 0000000000..bc0f8576d3 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/src/invariant.ts @@ -0,0 +1,20 @@ +/** Package-owned invariant companion. @module @deepseek-ai/dsh-client-ui-settings-plugin-control/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-settings-plugin-control' + +/** Cordis companion plugin name. */ +export const name = 'client-ui-settings-plugin-control-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** No runtime invariant: the slot registry owns contribution teardown and Connection owns the loopback trust fence. */ +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 */ diff --git a/packages/client/ui-settings-plugin-control/tests/browser-plugin.client.spec.tsx b/packages/client/ui-settings-plugin-control/tests/browser-plugin.client.spec.tsx new file mode 100644 index 0000000000..be43d9ae48 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/tests/browser-plugin.client.spec.tsx @@ -0,0 +1,116 @@ +// @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 { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' +import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' +import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' +import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import type { PluginControlId } from '@deepseek-ai/dsh-api-remotes/client' +import { apply as applyHostEntry } from '../src/index.ts' +import { apply, inject } from '../src/client/index.ts' +import { PluginControlSettingsTab } from '../src/client/PluginControlSettingsTab.tsx' +import type { PluginControlSettingsTabInjected } from '../src/client/PluginControlSettingsTab.tsx' + +usePinnedBrowserLanguages('zh-CN') +afterEach(cleanup) + +const SNAPSHOT = { + controls: [{ id: 'genui', name: 'dsh-genui', repository: 'https://example.com/genui', state: 'enabled' }], +} as const + +async function bench(isLoopback = true) { + const ctx = new Context() + await ctx.plugin(SlotRegistry).await() + const locale = new LocaleRuntime(ctx) + ctx.provide('locale', locale) + const call = vi.fn() + .mockResolvedValue({ ok: true, value: SNAPSHOT }) + class ConnectionService extends Service { + constructor(serviceCtx: Context) { + super(serviceCtx, 'connection') + Object.assign(this, { + isLoopback, + rpc: { call }, + }) + } + } + new ConnectionService(ctx) + return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, call } +} + +function declare(slots: SlotRegistry): () => void { + return slots.register({ + name: 'root', + children: { 'settings.plugins.tab': { kind: 'list', scope: 'root' } }, + } as never, () => null) +} + +function requiredAt(values: readonly T[], index: number): T { + const value = values[index] + if (value === undefined) throw new Error(`missing fixture value at index ${index}`) + return value +} + +describe('ui-settings-plugin-control browser plugin', () => { + it('keeps the Host loader entry inert', () => { + expect(applyHostEntry).toBeTypeOf('function') + applyHostEntry() + }) + + it('registers the third localized tab without reading the channel eagerly', async () => { + const b = await bench() + declare(b.slots) + await b.ctx.plugin({ inject: [...inject], apply }).await() + + const entry = requiredAt(b.slots.entries('settings.plugins.tab'), 0) + expect(entry.component).toBe(PluginControlSettingsTab) + expect(entry.options).toMatchObject({ id: 'controls', order: 20 }) + expect(resolveSlotLabel(entry.options.label)).toBe('插件开关') + expect(b.call).not.toHaveBeenCalled() + + const injected = (entry.inject as unknown as () => PluginControlSettingsTabInjected)() + expect(injected.isLoopback).toBe(true) + await expect(injected.list()).resolves.toEqual(SNAPSHOT) + expect(b.call).toHaveBeenLastCalledWith('/plugin-control', 'list', {}) + await expect(injected.setEnabled('genui' as PluginControlId, false)).resolves.toEqual(SNAPSHOT) + expect(b.call).toHaveBeenLastCalledWith('/plugin-control', 'set-enabled', { + pluginId: 'genui', enabled: false, + }) + + b.call.mockResolvedValueOnce({ + ok: false, + error: { code: 'internal', message: 'failed', details: {} }, + }) + await expect(injected.list()).rejects.toThrow('plugin-control list failed: internal: failed') + b.call.mockResolvedValueOnce({ ok: true, value: { controls: [{ invalid: true }] } }) + await expect(injected.list()).rejects.toThrow('response contains an invalid control') + await b.ctx.fiber.dispose() + }) + + it('follows locale, carries remote authority, and survives declaration reload', async () => { + const b = await bench(false) + const fiber = b.ctx.plugin({ inject: [...inject], apply }) + await fiber.await() + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) + + const stop = declare(b.slots) + await vi.waitFor(() => { expect(b.slots.entries('settings.plugins.tab')).toHaveLength(1) }) + const first = requiredAt(b.slots.entries('settings.plugins.tab'), 0) + const injected = (first.inject as unknown as () => PluginControlSettingsTabInjected)() + expect(injected.isLoopback).toBe(false) + b.locale.setLocale('en') + expect(resolveSlotLabel(first.options.label)).toBe('Plugin switches') + + stop() + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) + declare(b.slots) + await vi.waitFor(() => { + expect(b.slots.entries('settings.plugins.tab')[0]?.component).toBe(PluginControlSettingsTab) + }) + await fiber.dispose() + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) + await b.ctx.fiber.dispose() + }) +}) diff --git a/packages/client/ui-settings-plugin-control/tests/components.client.spec.tsx b/packages/client/ui-settings-plugin-control/tests/components.client.spec.tsx new file mode 100644 index 0000000000..2f8081088a --- /dev/null +++ b/packages/client/ui-settings-plugin-control/tests/components.client.spec.tsx @@ -0,0 +1,130 @@ +// @vitest-environment jsdom +import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { PluginControlSnapshot } from '@deepseek-ai/dsh-api-remotes/client' +import { PluginControlSettingsTab } from '../src/client/PluginControlSettingsTab.tsx' +import type { + PluginControlSettingsTabInjected, + PluginControlSettingsTabProps, +} from '../src/client/PluginControlSettingsTab.tsx' +import { en, type PluginControlLocaleKey } from '../src/client/locales.ts' + +afterEach(cleanup) + +const t = ((key: PluginControlLocaleKey, params?: Record): string => { + const value = en[key] + return Object.entries(params ?? {}).reduce( + (text, [name, replacement]) => text.replace(`{${name}}`, String(replacement)), + value, + ) +}) as PluginControlSettingsTabProps['t'] + +const SNAPSHOT = { + controls: [ + { id: 'genui', name: 'dsh-genui', repository: 'https://example.com/genui', state: 'enabled' }, + { id: 'annotation', name: 'dsh-annotation', repository: 'https://example.com/annotation', state: 'disabled' }, + { id: 'web-ui', name: 'dsh-web-ui', repository: 'https://example.com/web-ui', state: 'mixed' }, + { id: 'missing', name: 'missing-plugin', repository: 'https://example.com/missing', state: 'unavailable' }, + ], +} as unknown as PluginControlSnapshot + +function props( + overrides: Partial = {}, +): PluginControlSettingsTabProps { + return { + t, + isLoopback: true, + list: async () => SNAPSHOT, + setEnabled: async () => SNAPSHOT, + ...overrides, + } as PluginControlSettingsTabProps +} + +describe('PluginControlSettingsTab', () => { + it('renders aggregate states and applies accessible switches', async () => { + const listDeferred = Promise.withResolvers() + const setDeferred = Promise.withResolvers() + const setEnabled = vi.fn(() => setDeferred.promise) + render( listDeferred.promise, setEnabled })} />) + expect(screen.getByText(en.loading)).toBeTruthy() + await act(async () => { listDeferred.resolve(SNAPSHOT) }) + + expect(screen.getByRole('heading', { name: en.heading })).toBeTruthy() + expect(screen.getAllByRole('listitem')).toHaveLength(4) + for (const label of [en.enabled, en.disabled, en.mixed, en.unavailable]) { + expect(screen.getByText(label)).toBeTruthy() + } + expect(screen.getAllByRole('link', { name: en.source })).toHaveLength(4) + expect(screen.getByRole('switch', { name: 'Turn off dsh-genui' }).getAttribute('aria-checked')).toBe('true') + expect(screen.getByRole('switch', { name: 'Turn on dsh-annotation' }).getAttribute('aria-checked')).toBe('false') + expect(screen.getByRole('switch', { name: 'Turn on dsh-web-ui' }).getAttribute('aria-checked')).toBe('false') + expect(screen.getByRole('switch', { name: 'Turn on missing-plugin' })).toHaveProperty('disabled', true) + + fireEvent.click(screen.getByRole('switch', { name: 'Turn off dsh-genui' })) + await waitFor(() => { expect(setEnabled).toHaveBeenCalledWith('genui', false) }) + expect(screen.getByText(en.applying)).toBeTruthy() + expect(screen.getByRole('switch', { name: 'Turn on dsh-annotation' })).toHaveProperty('disabled', true) + + const next = { + controls: SNAPSHOT.controls.map(control => control.id === 'genui' ? { ...control, state: 'disabled' as const } : control), + } + await act(async () => { setDeferred.resolve(next) }) + expect(screen.getByRole('switch', { name: 'Turn on dsh-genui' }).getAttribute('aria-checked')).toBe('false') + expect(screen.queryByText(en.applying)).toBeNull() + expect(screen.getByRole('status').textContent).toBe(en.restartHint) + }) + + it('contains load and mutation failures and retries into the empty state', async () => { + const list = vi.fn() + .mockRejectedValueOnce(new Error('private transport detail')) + .mockResolvedValueOnce({ controls: [] }) + const setEnabled = vi.fn() + .mockRejectedValue(new Error('write detail')) + render() + + 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() + + cleanup() + render() + fireEvent.click(await screen.findByRole('switch', { name: 'Turn off dsh-genui' })) + expect((await screen.findByRole('alert')).textContent).toBe(en.updateError) + }) + + it('does not call the privileged route for a remote browser', () => { + const list = vi.fn() + render() + expect(screen.getByText(en.localOnlyTitle)).toBeTruthy() + expect(screen.getByText(en.localOnlyBody)).toBeTruthy() + expect(list).not.toHaveBeenCalled() + }) + + it('ignores late list and mutation settlements after unmount', async () => { + const listDeferred = Promise.withResolvers() + const pendingList = render( listDeferred.promise })} />) + pendingList.unmount() + await act(async () => { listDeferred.resolve(SNAPSHOT) }) + + const rejectedList = Promise.withResolvers() + const pendingListReject = render( + rejectedList.promise })} />, + ) + pendingListReject.unmount() + await act(async () => { rejectedList.reject(new Error('late list')) }) + + const setDeferred = Promise.withResolvers() + const pendingSet = render( setDeferred.promise })} />) + fireEvent.click(await screen.findByRole('switch', { name: 'Turn off dsh-genui' })) + pendingSet.unmount() + await act(async () => { setDeferred.resolve(SNAPSHOT) }) + + const rejected = Promise.withResolvers() + const pendingReject = render( rejected.promise })} />) + fireEvent.click(await screen.findByRole('switch', { name: 'Turn off dsh-genui' })) + pendingReject.unmount() + await act(async () => { rejected.reject(new Error('late')) }) + }) +}) diff --git a/packages/client/ui-settings-plugin-control/tests/protocol.client.spec.ts b/packages/client/ui-settings-plugin-control/tests/protocol.client.spec.ts new file mode 100644 index 0000000000..6ebbecd598 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/tests/protocol.client.spec.ts @@ -0,0 +1,29 @@ +// @vitest-environment jsdom +import { describe, expect, it } from 'vitest' +import { parsePluginControlSnapshot } from '../src/client/protocol.ts' + +const VALID = { + controls: [{ id: 'x', name: 'X', repository: 'https://example.com/x', state: 'enabled' }], +} + +describe('parsePluginControlSnapshot', () => { + it('accepts the complete state vocabulary', () => { + const controls = ['enabled', 'disabled', 'mixed', 'unavailable'].map(state => ({ + id: state, name: state, repository: `https://example.com/${state}`, state, + })) + expect(parsePluginControlSnapshot({ controls })).toEqual({ controls }) + }) + + it.each([ + null, + {}, + { controls: [null] }, + { controls: [{ ...VALID.controls[0], id: 1 }] }, + { controls: [{ ...VALID.controls[0], name: 1 }] }, + { controls: [{ ...VALID.controls[0], repository: 1 }] }, + { controls: [{ ...VALID.controls[0], state: 1 }] }, + { controls: [{ ...VALID.controls[0], state: 'future' }] }, + ])('rejects an invalid wire payload %#', (value) => { + expect(() => parsePluginControlSnapshot(value)).toThrow('plugin-control: response') + }) +}) diff --git a/packages/client/ui-settings-plugin-control/tsconfig.json b/packages/client/ui-settings-plugin-control/tsconfig.json new file mode 100644 index 0000000000..0b00456b3e --- /dev/null +++ b/packages/client/ui-settings-plugin-control/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../connection/tsconfig.client.json" + }, + { + "path": "../locale" + }, + { + "path": "../runtime" + }, + { + "path": "../ui-settings" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-slots" + }, + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/packages/client/ui-settings-plugin-control/tsdown.config.ts b/packages/client/ui-settings-plugin-control/tsdown.config.ts new file mode 100644 index 0000000000..6867d5fa02 --- /dev/null +++ b/packages/client/ui-settings-plugin-control/tsdown.config.ts @@ -0,0 +1,3 @@ +import { clientBundle } from '../tsdown.client.ts' + +export default clientBundle('@deepseek-ai/dsh-client-ui-settings-plugin-control', ['lib/types/index.js', 'lib/types/invariant.js']) diff --git a/packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.tsx b/packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.tsx index 95a242af92..6e832f1fc1 100644 --- a/packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.tsx +++ b/packages/client/ui-settings-plugins/src/client/PluginsSettingsSection.tsx @@ -40,7 +40,7 @@ export function PluginsSettingsSection({ t, renderSlot, useTabs }: PluginsSettin // A tab mounts only when first selected, then stays mounted while hidden so // local drafts, disclosure state, search, and the inventory snapshot survive - // switching between the two views. + // switching between views. useEffect(() => { if (active === undefined) return setVisitedIds((previous) => { diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 0cd1f51cd2..39f0470347 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1352,6 +1352,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ slotInject: '', declaredBy: 'an entry in \'settings.section\' (client-ui-settings-plugins), so it exists while that entry is mounted', occupants: [ + 'client-ui-settings-plugin-control PluginControlSettingsTab id \'controls\'', 'client-ui-settings-plugin-inventory PluginInventorySettingsTab id \'all\'', 'client-ui-settings-plugins ConfigurablePluginsTab id \'configurable\'', ], diff --git a/packages/host/plugin-control/README.i18n.yaml b/packages/host/plugin-control/README.i18n.yaml new file mode 100644 index 0000000000..a53f2e31f4 --- /dev/null +++ b/packages/host/plugin-control/README.i18n.yaml @@ -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-control/README.md +README.md: 49f72012fd7d0e897bdd1f16a9fbddc98dcf51f7 +README.zh.md: 4722fa385c28f668d08a836c692dd6b710924659 diff --git a/packages/host/plugin-control/README.md b/packages/host/plugin-control/README.md new file mode 100644 index 0000000000..49f72012fd --- /dev/null +++ b/packages/host/plugin-control/README.md @@ -0,0 +1,32 @@ +# @deepseek-ai/dsh-host-plugin-control + +English | [中文](README.zh.md) + +Loopback-only persistence for a deployment-owned set of logical plugin switches. The composing profile supplies an absolute `profilePatchPath` and a `controls` catalog; each catalog item has a stable control id, display name, HTTP(S) repository URL, and one or more profile-local Loader entry ids. `PluginControlGateway` resolves every local id to exactly one mounted entry, projects `enabled`, `disabled`, `mixed`, or `unavailable` state, and exposes `list` plus `set-enabled` on the generic Connection channel `/plugin-control`. + +`set-enabled` validates the requested control, serializes concurrent writes, and rewrites only YAML rows marked `# dsh-plugin-control: ` in the current profile's `cordis.patch.yml`. The writer uses the shared file lock and atomic publication helpers, creates a missing file with private permissions inside the existing profile directory, and preserves unrelated rows, comments, and `!!js` expressions. Community plugins are not required to support reversible runtime registration, so the running Loader tree is not mutated; the returned snapshot reflects the saved setting, and the next DSH process applies it through the ordinary profile patch order. + +The route accepts only loopback-authority requests. A remote browser cannot list or mutate controls through this channel, and callers cannot address an arbitrary Loader entry outside the deployment catalog. Invalid YAML, missing or ambiguous entries, unsafe repository URLs, duplicate ownership, and unknown control ids fail without replacing the existing patch file. + +## Model Experience + +### Restart-time plugin selection + +#### What the model sees + +Nothing from `plugin-control`. It registers no prompt, tool, message, or model provider; after DSH restarts, the selected plugins determine which of their own model-visible contributions are present. + +#### Token effect + +Zero in the running process. After restart, token changes belong to the enabled or disabled plugins. + +#### KV Cache effect + +None in the running process. After restart, enabling or disabling a plugin can change the request prefix or tool list according to that plugin's own behavior. + +## Known Limitations and Deferred Work + +- **Restart required** — switches persist desired state but do not unload or reload the current plugin fiber because third-party plugins may retain routes, tools, or other registrations after teardown. +- **Configured products only** — the endpoint controls only the logical catalog supplied by the deployment; it is not a general mutation API for the Loader inventory. +- **Later layers still win** — a home-level patch or command-line overlay applied after the profile patch can override the saved setting at the next boot. +- **No filesystem subscription** — direct edits made after startup are not reflected in the current gateway snapshot until the process restarts. diff --git a/packages/host/plugin-control/README.zh.md b/packages/host/plugin-control/README.zh.md new file mode 100644 index 0000000000..4722fa385c --- /dev/null +++ b/packages/host/plugin-control/README.zh.md @@ -0,0 +1,32 @@ +# @deepseek-ai/dsh-host-plugin-control + +[English](README.md) | 中文 + +为部署方拥有的一组逻辑插件开关提供仅限回环访问的持久化能力。组装 profile 提供绝对路径 `profilePatchPath` 和 `controls` 清单;每个清单项都包含稳定的控制 id、显示名称、HTTP(S) 仓库 URL,以及一个或多个 profile 本地 Loader 条目 id。`PluginControlGateway` 要求每个本地 id 恰好解析为一个已挂载条目,投影 `enabled`、`disabled`、`mixed` 或 `unavailable` 状态,并通过通用 Connection 通道 `/plugin-control` 暴露 `list` 与 `set-enabled`。 + +`set-enabled` 会校验请求的控制项、串行执行并发写入,并且只改写当前 profile 的 `cordis.patch.yml` 中带有 `# dsh-plugin-control: ` 标记的 YAML 行。写入器使用共用的文件锁与原子发布辅助函数,在已有 profile 目录内以私有权限创建缺失的文件,并保留无关行、注释及 `!!js` 表达式。社区插件不必支持可逆的运行时注册,因此不会修改正在运行的 Loader 树;返回快照表示已保存的设置,下一个 DSH 进程会通过普通 profile patch 顺序应用它。 + +该路由只接受具有回环权限的请求。远程浏览器不能通过此通道读取或修改控制项,调用方也不能访问部署清单之外的任意 Loader 条目。YAML 无效、条目缺失或存在歧义、仓库 URL 不安全、所有权重复及未知控制 id 都会明确失败,并保留现有 patch 文件。 + +## 模型体验 + +### 重启期插件选择 + +#### 模型看到的内容 + +`plugin-control` 自身没有任何内容。它不注册提示词、工具、消息或模型提供方;DSH 重启后,由所选插件决定其各自的模型可见贡献是否存在。 + +#### Token 影响 + +当前进程中为零。重启后的 token 变化属于启用或关闭的插件。 + +#### KV Cache 影响 + +对当前进程没有影响。重启后,启用或停用插件可能依照该插件自身行为改变请求前缀或工具列表。 + +## 已知限制与暂缓事项 + +- **需要重启** —— 开关会持久化期望状态,但不会卸载或重新加载当前插件 fiber,因为第三方插件 teardown 后可能仍保留路由、工具或其他注册。 +- **只控制配置的产品** —— 端点只控制部署方提供的逻辑清单,并不是 Loader 清单的通用修改 API。 +- **后续层仍有更高优先级** —— 应用在 profile patch 之后的 home 级 patch 或命令行 overlay,可能在下次启动时覆盖已保存设置。 +- **不订阅文件系统变化** —— 启动后直接编辑文件,不会反映在当前 gateway 快照中,直至进程重启。 diff --git a/packages/host/plugin-control/package.json b/packages/host/plugin-control/package.json new file mode 100644 index 0000000000..4745810e25 --- /dev/null +++ b/packages/host/plugin-control/package.json @@ -0,0 +1,60 @@ +{ + "name": "@deepseek-ai/dsh-host-plugin-control", + "description": "Loopback-only persistent enablement controls for configured profile plugins", + "version": "0.1.0-rc.5", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/host/plugin-control" + }, + "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" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "workspace:^", + "yaml": "^2.8.2", + "zod": "^4.4.3" + }, + "peerDependencies": { + "@deepseek-ai/cordis-plugin-loader": "workspace:^", + "@deepseek-ai/dsh-atomic-write": "workspace:^", + "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/cordis-plugin-loader": "workspace:^", + "@deepseek-ai/dsh-atomic-write": "workspace:^", + "@deepseek-ai/dsh-brand": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/host/plugin-control/src/control-file.ts b/packages/host/plugin-control/src/control-file.ts new file mode 100644 index 0000000000..c2595bef35 --- /dev/null +++ b/packages/host/plugin-control/src/control-file.ts @@ -0,0 +1,88 @@ +/** Profile-patch persistence for loopback plugin controls. */ + +import { readFile } from 'node:fs/promises' +import { isMap, isSeq, parseDocument, type ScalarTag } from 'yaml' +import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' + +const MARKER_PREFIX = 'dsh-plugin-control:' +const JS_EXPRESSION_TAG: ScalarTag = { + tag: 'tag:yaml.org,2002:js', + resolve: value => value, +} + +/** Values required to persist one configured logical control. */ +export interface PersistedPluginControl { + /** Stable marker written into YAML comments. */ + readonly id: string + /** Loader entry ids receiving the same override. */ + readonly entryIds: readonly string[] +} + +/** Whether an unknown filesystem failure reports a missing file. */ +function isMissing(error: unknown): boolean { + /* v8 ignore next -- node:fs promise rejections are ErrnoException objects, never null or undefined. */ + return (error as NodeJS.ErrnoException | null)?.code === 'ENOENT' +} + +/** Whether one YAML item was previously emitted for `controlId`. */ +function isManagedItem(item: unknown, controlId: string): boolean { + if (!isMap(item)) return false + const expected = `${MARKER_PREFIX} ${controlId}` + return (item.commentBefore ?? '').split('\n').some(line => line.trim() === expected) +} + +/** + * Replace one control's managed id patches while preserving every unowned YAML + * node, comment, and `!!js` expression in the profile layer. + * @param filename - absolute profile `cordis.patch.yml` path. + * @param control - logical identity and governed Loader entries. + * @param enabled - desired explicit enablement persisted after user patches. + */ +export async function writePluginControlState( + filename: string, + control: PersistedPluginControl, + enabled: boolean, +): Promise { + await withFileLock(filename, async () => { + let text: string + try { + text = await readFile(filename, 'utf8') + } catch (error) { + if (!isMissing(error)) throw error + text = '[]\n' + } + const document = parseDocument(text, { + customTags: [JS_EXPRESSION_TAG], + prettyErrors: true, + uniqueKeys: true, + }) + if (document.errors.length > 0) { + const firstError = document.errors[0] + /* v8 ignore next 2 -- a positive errors.length guarantees index zero exists. */ + if (firstError === undefined) { + throw new Error(`plugin-control: cannot update invalid YAML at ${filename}`) + } + throw new Error(`plugin-control: cannot update invalid YAML at ${filename}: ${firstError.message}`) + } + if (!isSeq(document.contents)) { + throw new Error(`plugin-control: ${filename} must contain a top-level YAML array`) + } + + const items = document.contents.items + for (let index = items.length - 1; index >= 0; index -= 1) { + if (isManagedItem(items[index], control.id)) items.splice(index, 1) + } + for (const entryId of control.entryIds) { + const item = document.createNode({ id: entryId, disabled: !enabled }) + /* v8 ignore next -- createNode(object) is guaranteed to return a YAML map. */ + if (!isMap(item)) { + throw new Error('plugin-control: YAML library did not create a map node') + } + item.commentBefore = ` ${MARKER_PREFIX} ${control.id}` + // parseDocument narrows the sequence to range-bearing ParsedNode values, + // while Document.createNode correctly returns a fresh pre-stringify node. + items.push(item as never) + } + await writeFileAtomic(filename, document.toString({ lineWidth: 0 }), { mode: 0o600, dirMode: 0o700 }) + }) +} diff --git a/packages/host/plugin-control/src/index.ts b/packages/host/plugin-control/src/index.ts new file mode 100644 index 0000000000..add7cd0bc4 --- /dev/null +++ b/packages/host/plugin-control/src/index.ts @@ -0,0 +1,245 @@ +/** Loopback-only control of configured plugin products in the current profile. */ + +import { isAbsolute } from 'node:path' +import type { Context } from '@deepseek-ai/cordis' +import type { Entry } from '@deepseek-ai/cordis-plugin-loader' +import schema from '@deepseek-ai/schemastery' +import { z } from 'zod' +import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection' +import { writePluginControlState } from './control-file.ts' +import type { + PluginControlId, + PluginControlItem, + PluginControlSnapshot, + PluginControlState, + SetPluginEnabledRequest, +} from './types.ts' + +export type * from './types.ts' + +const CHANNEL = '/plugin-control' +const LIST_ENDPOINT = 'list' +const SET_ENABLED_ENDPOINT = 'set-enabled' +const CONTROL_ID_PATTERN = /^[A-Za-z0-9._~-]+$/ + +const emptyRequestSchema = z.object({}).strict() +const setEnabledRequestSchema = z.object({ + pluginId: z.string().min(1), + enabled: z.boolean(), +}).strict() + +/** One deployment-configured logical control and its Loader rows. */ +export interface PluginControlSpec { + /** Stable id used in the profile patch marker and mutation request. */ + id: string + /** Human-readable product name shown in Settings. */ + name: string + /** HTTP(S) source repository shown in Settings. */ + repository: string + /** Complete Loader entry-id set controlled as one product. */ + entryIds: string[] +} + +/** Plugin-control gateway configuration owned by the composing profile. */ +export interface Config { + /** Absolute user patch layer of the running profile. */ + profilePatchPath: string + /** Logical products that this deployment permits the browser to control. */ + controls: PluginControlSpec[] +} + +const controlSpecSchema = schema.object({ + id: schema.string().min(1).required(), + name: schema.string().min(1).required(), + repository: schema.string().min(1).required(), + entryIds: schema.array(schema.string().min(1)).min(1).required(), +}) + +/** Validate the profile-owned control catalog before the route becomes reachable. */ +export const Config: schema = schema.object({ + profilePatchPath: schema.string().min(1).required(), + controls: schema.array(controlSpecSchema).min(1).required(), +}) + +/** Services required by the loopback RPC adapter. */ +export const inject = ['loader', 'connection'] + +/** Brand a deployment-validated control id for the public response. */ +function pluginControlId(value: string): PluginControlId { + return value as PluginControlId +} + +/** Error text for a caught request or lifecycle failure. */ +function messageOf(error: unknown): string { + if (error instanceof AggregateError) { + const details = error.errors.map(messageOf).join('; ') + return details === '' ? error.message : `${error.message}: ${details}` + } + return error instanceof Error ? error.message : String(error) +} + +/** Validate relations that Schemastery cannot express locally. */ +function validateConfig(config: Config): void { + if (!isAbsolute(config.profilePatchPath)) { + throw new Error('plugin-control: profilePatchPath must be absolute') + } + const controlIds = new Set() + const entryIds = new Set() + for (const control of config.controls) { + if (control.id.trim() !== control.id || !CONTROL_ID_PATTERN.test(control.id)) { + throw new Error(`plugin-control: invalid control id ${JSON.stringify(control.id)}`) + } + if (controlIds.has(control.id)) { + throw new Error(`plugin-control: duplicate control id ${JSON.stringify(control.id)}`) + } + controlIds.add(control.id) + if (control.name.trim() !== control.name) { + throw new Error(`plugin-control: control ${JSON.stringify(control.id)} has surrounding whitespace in its name`) + } + let repository: URL + try { + repository = new URL(control.repository) + } catch (cause) { + throw new Error(`plugin-control: control ${JSON.stringify(control.id)} has an invalid repository URL`, { cause }) + } + if (repository.protocol !== 'https:' && repository.protocol !== 'http:') { + throw new Error(`plugin-control: control ${JSON.stringify(control.id)} repository must use HTTP(S)`) + } + for (const entryId of control.entryIds) { + if (entryId.trim() !== entryId) { + throw new Error(`plugin-control: control ${JSON.stringify(control.id)} has surrounding whitespace in an entry id`) + } + if (entryIds.has(entryId)) { + throw new Error(`plugin-control: Loader entry ${JSON.stringify(entryId)} belongs to more than one control`) + } + entryIds.add(entryId) + } + } +} + +/** Loopback RPC adapter over Loader state and the active profile patch layer. */ +export class PluginControlGateway { + private mutationTail: Promise = Promise.resolve() + private readonly desired = new Map() + + /** + * Validate the catalog and retain its owning context. + * @param ctx - plugin context carrying Loader and Connection. + * @param config - resolved profile path and logical controls. + */ + constructor(private readonly ctx: Context, private readonly config: Config) { + validateConfig(config) + } + + /** Resolve profile-local ids to the unique entries in the mounted Include tree. */ + private resolveEntries(entryIds: readonly string[]): { entries: Entry[]; unresolved: string[] } { + const mounted = [...this.ctx.loader.entries()] + const entries: Entry[] = [] + const unresolved: string[] = [] + for (const entryId of entryIds) { + const matches = mounted.filter(entry => entry.options.id === entryId) + if (matches.length !== 1) { + unresolved.push(matches.length === 0 ? entryId : `${entryId} (${matches.length} matches)`) + continue + } + const entry = matches[0] + /* v8 ignore next 2 -- exactly one array match guarantees index zero exists. */ + if (entry === undefined) { + throw new Error(`plugin-control: resolved entry ${JSON.stringify(entryId)} disappeared`) + } + entries.push(entry) + } + return { entries, unresolved } + } + + /** + * Return a point-in-time aggregate state for every configured control. + * @returns configured controls and their saved or running aggregate states. + */ + list(): PluginControlSnapshot { + return { + controls: this.config.controls.map((control): PluginControlItem => { + const controlled = this.resolveEntries(control.entryIds) + let state: PluginControlState + if (controlled.unresolved.length > 0) { + state = 'unavailable' + } else if (this.desired.has(control.id)) { + state = this.desired.get(control.id) === true ? 'enabled' : 'disabled' + } else { + const disabled = controlled.entries.map(entry => entry.disabled) + state = disabled.every(Boolean) ? 'disabled' : disabled.some(Boolean) ? 'mixed' : 'enabled' + } + return { + id: pluginControlId(control.id), + name: control.name, + repository: control.repository, + state, + } + }), + } + } + + /** Serialize same-process mutations so durable settings retain request order. */ + private enqueue(operation: () => Promise): Promise { + const result = this.mutationTail.then(operation, operation) + this.mutationTail = result.then(() => {}, () => {}) + return result + } + + /** Persist one setting for the next process start. */ + private setEnabled(request: SetPluginEnabledRequest, signal: AbortSignal): Promise { + const control = this.config.controls.find(candidate => candidate.id === request.pluginId) + if (control === undefined) { + return Promise.reject(new Error(`unknown plugin control ${JSON.stringify(request.pluginId)}`)) + } + return this.enqueue(async () => { + if (signal.aborted) throw signal.reason + const controlled = this.resolveEntries(control.entryIds) + if (controlled.unresolved.length > 0) { + throw new Error(`plugin control ${JSON.stringify(control.id)} is unavailable; unresolved Loader entries: ${controlled.unresolved.join(', ')}`) + } + await writePluginControlState(this.config.profilePatchPath, control, request.enabled) + // Community plugins are not required to support reversible lifecycle + // registration, so the running tree stays untouched until restart. + this.desired.set(control.id, request.enabled) + return this.list() + }) + } + + /** Decoded Connection handler for list and mutation endpoints. */ + readonly handle: ConnectionRpcHandler = async (endpoint, payload, signal) => { + if (endpoint === LIST_ENDPOINT) { + const parsed = emptyRequestSchema.safeParse(payload) + if (!parsed.success) { + return { ok: false, error: { code: 'bad-request', message: 'invalid plugin-control list request', details: { issues: parsed.error.issues } } } + } + return { ok: true, value: this.list() } + } + if (endpoint !== SET_ENABLED_ENDPOINT) { + return { ok: false, error: { code: 'bad-request', message: `unknown plugin-control endpoint ${JSON.stringify(endpoint)}`, details: { issues: [] } } } + } + const parsed = setEnabledRequestSchema.safeParse(payload) + if (!parsed.success) { + return { ok: false, error: { code: 'bad-request', message: 'invalid plugin-control mutation request', details: { issues: parsed.error.issues } } } + } + if (signal.aborted) { + return { ok: false, error: { code: 'cancelled', message: 'plugin-control request was cancelled', details: {} } } + } + try { + const value = await this.setEnabled(parsed.data as SetPluginEnabledRequest, signal) + return { ok: true, value } + } catch (error) { + const result = { + ok: false, + error: { code: 'internal' as const, message: messageOf(error), details: {} }, + } as const + return result + } + } +} + +/** Register the configured gateway on a loopback-only Connection channel. */ +export function apply(ctx: Context, config: Config): void { + const gateway = new PluginControlGateway(ctx, config) + ctx.connection.rpc.handle(CHANNEL, gateway.handle, { authority: 'loopback' }) +} diff --git a/packages/host/plugin-control/src/invariant.ts b/packages/host/plugin-control/src/invariant.ts new file mode 100644 index 0000000000..957ec8a6db --- /dev/null +++ b/packages/host/plugin-control/src/invariant.ts @@ -0,0 +1,20 @@ +/** Package-owned invariant companion. @module @deepseek-ai/dsh-host-plugin-control/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-control' + +/** Cordis companion plugin name. */ +export const name = 'host-plugin-control-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** No runtime invariant: each read checks the live Loader relation and each write is lock-serialized and schema-validated. */ +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 */ diff --git a/packages/host/plugin-control/src/types.ts b/packages/host/plugin-control/src/types.ts new file mode 100644 index 0000000000..8a6f934c83 --- /dev/null +++ b/packages/host/plugin-control/src/types.ts @@ -0,0 +1,30 @@ +import type { Branded } from '@deepseek-ai/dsh-brand' + +/** Stable deployment-defined identity of one logical plugin control. */ +export type PluginControlId = Branded<'PluginControlId'> + +/** Saved or running aggregate state of the Loader entries governed by one logical control. */ +export type PluginControlState = 'enabled' | 'disabled' | 'mixed' | 'unavailable' + +/** One logical plugin product exposed to the browser control panel. */ +export interface PluginControlItem { + /** Stable control identity used by mutation requests. */ + readonly id: PluginControlId + /** Human-readable product name supplied by the composing profile. */ + readonly name: string + /** Upstream source repository for attribution and inspection. */ + readonly repository: string + /** Saved desired state, or aggregate running state before a same-process write. */ + readonly state: PluginControlState +} + +/** Point-in-time control catalog returned by the loopback RPC channel. */ +export interface PluginControlSnapshot { + readonly controls: readonly PluginControlItem[] +} + +/** Request that persists the next-start state of every Loader entry owned by one logical control. */ +export interface SetPluginEnabledRequest { + readonly pluginId: PluginControlId + readonly enabled: boolean +} diff --git a/packages/host/plugin-control/tests/control.spec.ts b/packages/host/plugin-control/tests/control.spec.ts new file mode 100644 index 0000000000..c086d4b82a --- /dev/null +++ b/packages/host/plugin-control/tests/control.spec.ts @@ -0,0 +1,285 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Context, type Plugin } from '@deepseek-ai/cordis' +import Loader from '@deepseek-ai/cordis-plugin-loader' +import type { ConnectionRpcHandler, HostConnectionHandle } from '@deepseek-ai/dsh-client-connection' +import { + Config, + PluginControlGateway, + apply, + inject, + type Config as PluginControlConfig, + type PluginControlSpec, +} from '../src/index.ts' +import { writePluginControlState } from '../src/control-file.ts' + +const contexts: Context[] = [] +const tempRoots: string[] = [] + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) + await Promise.all(tempRoots.splice(0).map(root => rm(root, { recursive: true, force: true }))) +}) + +const activePlugin: Plugin.Function = () => {} + +async function harness(entryCount = 2): Promise<{ + ctx: Context + entryIds: string[] + path: string +}> { + const root = await mkdtemp(join(tmpdir(), 'dsh-plugin-control-')) + tempRoots.push(root) + const path = join(root, 'cordis.patch.yml') + await writeFile(path, '[]\n') + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(Loader) + ctx.loader.builtins.active = activePlugin + const entryIds: string[] = [] + for (let index = 0; index < entryCount; index += 1) { + entryIds.push(await ctx.loader.create({ name: 'cordis:active' })) + } + return { ctx, entryIds, path } +} + +function control( + entryIds: string[], + options: { id?: string; name?: string; repository?: string } = {}, +): PluginControlSpec { + return { + id: options.id ?? 'fixture', + name: options.name ?? 'Fixture plugin', + repository: options.repository ?? 'https://example.com/plugin', + entryIds, + } +} + +function config(path: string, entryIds: string[]): PluginControlConfig { + return { + profilePatchPath: path, + controls: [control(entryIds)], + } +} + +function successfulValue(result: Awaited>): unknown { + if (!result.ok) throw new Error(result.error.message) + return result.value +} + +function requiredAt(values: readonly T[], index: number): T { + const value = values[index] + if (value === undefined) throw new Error(`missing fixture value at index ${index}`) + return value +} + +describe('PluginControlGateway', () => { + it('registers one loopback-only channel through the plugin lifecycle', async () => { + const h = await harness(1) + let handler: ConnectionRpcHandler | undefined + const handle = vi.fn((channel, next, options) => { + expect(channel).toBe('/plugin-control') + expect(options).toEqual({ authority: 'loopback' }) + handler = next + return async () => {} + }) + h.ctx.provide('connection', { rpc: { handle, intercept: vi.fn() } }) + + const fiber = h.ctx.plugin({ Config, inject, apply }, config(h.path, h.entryIds)) + await fiber.await() + expect(handle).toHaveBeenCalledOnce() + if (handler === undefined) throw new Error('plugin-control handler was not registered') + expect(successfulValue(await handler('list', {}, new AbortController().signal))).toMatchObject({ + controls: [{ id: 'fixture', state: 'enabled' }], + }) + + await fiber.dispose() + }) + + it('projects enabled, mixed, disabled, and unavailable aggregate states', async () => { + const h = await harness(2) + const gateway = new PluginControlGateway(h.ctx, config(h.path, h.entryIds)) + expect(gateway.list().controls[0]?.state).toBe('enabled') + + await h.ctx.loader.update(requiredAt(h.entryIds, 0), { disabled: true }) + expect(gateway.list().controls[0]?.state).toBe('mixed') + await h.ctx.loader.update(requiredAt(h.entryIds, 1), { disabled: true }) + expect(gateway.list().controls[0]?.state).toBe('disabled') + + await h.ctx.loader.remove(requiredAt(h.entryIds, 0)) + expect(gateway.list().controls[0]?.state).toBe('unavailable') + }) + + it('reports duplicate mounted local ids as unavailable', async () => { + const h = await harness(1) + const gateway = new PluginControlGateway(h.ctx, config(h.path, h.entryIds)) + const entry = h.ctx.loader.resolve(requiredAt(h.entryIds, 0)) + const entries = vi.spyOn(h.ctx.loader, 'entries').mockImplementation(function* () { + yield entry + yield entry + }) + + expect(gateway.list().controls[0]?.state).toBe('unavailable') + const result = await gateway.handle( + 'set-enabled', { pluginId: 'fixture', enabled: false }, new AbortController().signal, + ) + expect(result.ok).toBe(false) + if (result.ok) throw new Error('ambiguous control request unexpectedly succeeded') + expect(result.error.message).toContain('(2 matches)') + entries.mockRestore() + }) + + it('preserves user YAML and serializes restart-time settings without mutating the running tree', async () => { + const h = await harness(2) + await writeFile(h.path, `# user-owned comment +- id: manual + config: + value: !!js ctx.value +`) + const gateway = new PluginControlGateway(h.ctx, config(h.path, h.entryIds)) + + const [disabled, enabled] = await Promise.all([ + gateway.handle('set-enabled', { pluginId: 'fixture', enabled: false }, new AbortController().signal), + gateway.handle('set-enabled', { pluginId: 'fixture', enabled: true }, new AbortController().signal), + ]) + expect(disabled.ok).toBe(true) + expect(enabled.ok).toBe(true) + expect(gateway.list().controls[0]?.state).toBe('enabled') + expect(h.entryIds.map(entryId => h.ctx.loader.resolve(entryId).disabled)).toEqual([false, false]) + + const text = await readFile(h.path, 'utf8') + expect(text).toContain('# user-owned comment') + expect(text).toContain('value: !!js ctx.value') + expect(text.match(/# dsh-plugin-control: fixture/g)).toHaveLength(2) + expect(text.match(/disabled: false/g)).toHaveLength(2) + expect(text).not.toContain('disabled: true') + }) + + it('rejects malformed requests, cancellation, unavailable controls, and invalid YAML without mutation', async () => { + const h = await harness(1) + const gateway = new PluginControlGateway(h.ctx, config(h.path, h.entryIds)) + await expect(gateway.handle('list', { extra: true }, new AbortController().signal)) + .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } }) + await expect(gateway.handle('unknown', {}, new AbortController().signal)) + .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } }) + await expect(gateway.handle('set-enabled', { pluginId: '', enabled: true }, new AbortController().signal)) + .resolves.toMatchObject({ ok: false, error: { code: 'bad-request' } }) + + const cancelled = new AbortController() + cancelled.abort(new Error('left')) + await expect(gateway.handle('set-enabled', { pluginId: 'fixture', enabled: false }, cancelled.signal)) + .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } }) + const unknown = await gateway.handle( + 'set-enabled', { pluginId: 'missing', enabled: false }, new AbortController().signal, + ) + expect(unknown.ok).toBe(false) + if (unknown.ok) throw new Error('unknown control request unexpectedly succeeded') + expect(unknown.error).toMatchObject({ code: 'internal' }) + expect(unknown.error.message).toContain('unknown') + + await h.ctx.loader.remove(requiredAt(h.entryIds, 0)) + const unavailable = await gateway.handle( + 'set-enabled', { pluginId: 'fixture', enabled: false }, new AbortController().signal, + ) + expect(unavailable.ok).toBe(false) + if (unavailable.ok) throw new Error('unavailable control request unexpectedly succeeded') + expect(unavailable.error).toMatchObject({ code: 'internal' }) + expect(unavailable.error.message).toContain('unavailable') + expect(await readFile(h.path, 'utf8')).toBe('[]\n') + + const h2 = await harness(1) + const invalid = '- id: [not closed\n' + await writeFile(h2.path, invalid) + const invalidGateway = new PluginControlGateway(h2.ctx, config(h2.path, h2.entryIds)) + const invalidResult = await invalidGateway.handle( + 'set-enabled', { pluginId: 'fixture', enabled: false }, new AbortController().signal, + ) + expect(invalidResult.ok).toBe(false) + if (invalidResult.ok) throw new Error('invalid YAML request unexpectedly succeeded') + expect(invalidResult.error).toMatchObject({ code: 'internal' }) + expect(invalidResult.error.message).toContain('invalid YAML') + expect(await readFile(h2.path, 'utf8')).toBe(invalid) + expect(invalidGateway.list().controls[0]?.state).toBe('enabled') + }) + + it('formats queued cancellation reasons without touching the profile', async () => { + const h = await harness(1) + const gateway = new PluginControlGateway(h.ctx, config(h.path, h.entryIds)) + for (const [reason, message] of [ + [new AggregateError([], 'empty aggregate'), 'empty aggregate'], + [new AggregateError([new Error('nested'), 'plain'], 'outer'), 'outer: nested; plain'], + ] as const) { + const cancelled = new AbortController() + const pending = gateway.handle( + 'set-enabled', { pluginId: 'fixture', enabled: false }, cancelled.signal, + ) + cancelled.abort(reason) + const result = await pending + expect(result.ok).toBe(false) + if (result.ok) throw new Error('queued cancelled request unexpectedly succeeded') + expect(result.error.message).toBe(message) + } + expect(await readFile(h.path, 'utf8')).toBe('[]\n') + }) + + it('creates a missing patch, preserves unowned YAML items, and rejects wrong file types', async () => { + const h = await harness(1) + const persisted = control(h.entryIds) + await rm(h.path) + await writePluginControlState(h.path, persisted, false) + expect(await readFile(h.path, 'utf8')).toContain('disabled: true') + + await writeFile(h.path, '- scalar\n- id: plain\n') + await writePluginControlState(h.path, persisted, true) + const preserved = await readFile(h.path, 'utf8') + expect(preserved).toContain('- scalar') + expect(preserved).toContain('id: plain') + expect(preserved).toContain('disabled: false') + + await writeFile(h.path, '{}\n') + await expect(writePluginControlState(h.path, persisted, true)).rejects.toThrow('top-level YAML array') + + const directory = `${h.path}.directory` + await mkdir(directory) + await expect(writePluginControlState(directory, persisted, true)).rejects.toMatchObject({ code: 'EISDIR' }) + }) + + it('fails loud on ambiguous or unsafe deployment catalogs', async () => { + const h = await harness(2) + expect(() => new PluginControlGateway(h.ctx, config('relative.yml', h.entryIds))) + .toThrow('profilePatchPath must be absolute') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control(h.entryIds, { id: 'bad id' })], + })).toThrow('invalid control id') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control(h.entryIds), control(['other'])], + })).toThrow('duplicate control id') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control(h.entryIds, { name: ' padded ' })], + })).toThrow('surrounding whitespace in its name') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control(h.entryIds, { repository: 'not a URL' })], + })).toThrow('invalid repository URL') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control(h.entryIds, { repository: 'file:///tmp/plugin' })], + })).toThrow('must use HTTP(S)') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [control([' padded '])], + })).toThrow('surrounding whitespace in an entry id') + expect(() => new PluginControlGateway(h.ctx, { + profilePatchPath: h.path, + controls: [ + control([requiredAt(h.entryIds, 0)]), + control([requiredAt(h.entryIds, 0)], { id: 'second' }), + ], + })).toThrow('belongs to more than one control') + }) +}) diff --git a/packages/host/plugin-control/tsconfig.json b/packages/host/plugin-control/tsconfig.json new file mode 100644 index 0000000000..7c1e78b689 --- /dev/null +++ b/packages/host/plugin-control/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/loader" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../util/atomic-write" + }, + { + "path": "../../util/brand" + }, + { + "path": "../../client/connection/tsconfig.host.json" + }, + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/patches/@linxin666__dsh-client-ui-skin-center.patch b/patches/@linxin666__dsh-client-ui-skin-center.patch new file mode 100644 index 0000000000..7e0826c460 --- /dev/null +++ b/patches/@linxin666__dsh-client-ui-skin-center.patch @@ -0,0 +1,148 @@ +diff --git a/lib/index.js b/lib/index.js +index 1a10d14f8ee8d8ee3594be06d341635712ec7a64..5ab903f752ff7ae6aa96b7062e84c62929b80c22 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -4,6 +4,24 @@ import { lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSy + import { dirname, join } from "node:path"; + import { fileURLToPath } from "node:url"; + import { homedir } from "node:os"; ++/** ++* Bundled deployments (npm/pnpm flat layouts) ship the skins tree as a ++* `skins/` directory beside an ancestor `node_modules` (the deployment-owned ++* assembly); the checkout-relative location stays first so the upstream repo ++* layout keeps working unchanged. ++*/ ++const SKINS_DIR_LOCAL = fileURLToPath(new URL("../../../skins/", import.meta.url)); ++function resolveSkinsDir() { ++ if (statSync(SKINS_DIR_LOCAL, { throwIfNoEntry: false })) return SKINS_DIR_LOCAL; ++ let current = fileURLToPath(new URL("../../", import.meta.url)); ++ for (let depth = 0; depth < 12; depth += 1) { ++ current = dirname(current); ++ const candidate = join(current, "skins"); ++ if (statSync(candidate, { throwIfNoEntry: false })) return candidate; ++ } ++ return SKINS_DIR_LOCAL; ++} ++const RESOLVED_SKINS_DIR = resolveSkinsDir(); + //#region src/skin-switch.ts + /** + * In-process skin switching for the skin center — the official `dsh-skin use` +@@ -23,7 +41,7 @@ import { homedir } from "node:os"; + * @module @linxin666/dsh-client-ui-skin-center/skin-switch + */ + /** Repo layout: skin bundles live at packages/skins/. */ +-const SKINS_DIR$1 = fileURLToPath(new URL("../../../skins/", import.meta.url)); ++const SKINS_DIR$1 = RESOLVED_SKINS_DIR; + /** Managed patch-section delimiters (the CLI's SINGLE authority boundaries). */ + const MANAGED_START = "# --- dsh-skin managed (auto-generated; do not edit) ---"; + const MANAGED_END = "# --- end dsh-skin managed ---"; +@@ -122,13 +140,12 @@ function stripManaged(patch) { + * @param registry - registry to render against (defaults to the repo registry). + */ + function renderManaged(active, registry = loadRegistry()) { +- const wired = wiredNames(registry); + const lines = [MANAGED_START]; + for (const name of Object.keys(registry)) { + if (name === active) continue; + lines.push(`- id: ${registry[name].id}`, " disabled: true"); + } +- if (active !== null && !wired.has(active)) lines.push("- insert:", ` - id: ${registry[active].id}`, ` name: '${registry[active].pkg}'`); ++ if (active !== null) lines.push("- insert:", ` - id: ${registry[active].id}`, ` name: '${registry[active].pkg}'`); + lines.push(MANAGED_END); + return lines.join("\n"); + } +@@ -436,7 +453,7 @@ function postRoute(path, run) { + }; + } + /** Repo layout: skin bundles live at packages/skins//lib/client.js. */ +-const SKINS_DIR = fileURLToPath(new URL("../../../skins/", import.meta.url)); ++const SKINS_DIR = RESOLVED_SKINS_DIR; + /** + * Map skin id -> directory under packages/skins/, scanned from each + * skin.json. The id is validated against this map (never used as a raw +@@ -522,6 +539,53 @@ function bundleRoute() { + * Build the skin-center route family. + * @param deps - optional runner override (tests). + */ ++/** ++* Reconcile the running Loader tree with a freshly written managed patch. ++* The upstream flow relies on the host config watcher hot-reloading the ++* patch; bundled deployments without that watcher (packaged Electron) ++* instead update the live rows here, so the next page refresh boots the ++* graph the patch already describes. The patch file stays the authority: ++* a restart rebuilds the same tree from it. ++* @param ctx - cordis context (loader injected). ++* @returns a reconcile callback for an applied skin id (null = official). ++*/ ++function makeLiveReconcile(ctx) { ++ const registry = loadRegistry(); ++ const wired = wiredNames(registry); ++ const rowOf = (skin) => { ++ for (const entry of ctx.loader.entries()) { ++ const options = entry.options; ++ if (options === void 0) continue; ++ if (options.name === skin.pkg || options.id === skin.id) return entry; ++ } ++ return void 0; ++ }; ++ return async (active) => { ++ // Toggle `disabled` on mounted rows and create missing active rows. ++ // The update path re-cycles the row's fiber (dispose/init), which the ++ // client roster needs to observe the change; a bare disabled flag ++ // write would not dirty it. The patch file stays the authority: its ++ // managed section carries the same rows, so a restart rebuilds the ++ // same tree. ++ for (const [skinName, skin] of Object.entries(registry)) { ++ const enabled = skinName === active; ++ const row = rowOf(skin); ++ if (row !== void 0) { ++ try { ++ await ctx.loader.update(row.id, { disabled: !enabled }); ++ } catch (error) { ++ console.error("[ui-skin-center] live reconcile failed for", skin.pkg, error); ++ } ++ } else if (enabled) { ++ try { ++ await ctx.loader.create({ name: skin.pkg }); ++ } catch (error) { ++ console.error("[ui-skin-center] live row creation failed for", skin.pkg, error); ++ } ++ } ++ } ++ }; ++} + function makeSkinCenterRoutes(deps = {}) { + const run = deps.run ?? runDshSkin; + const current = () => run(["current"]).then((out) => out.trim() || "none"); +@@ -538,6 +602,13 @@ function makeSkinCenterRoutes(deps = {}) { + if (skin !== void 0) throw new Error("invalid-skin: skin and official are mutually exclusive"); + } else if (typeof skin !== "string" || skin === "") throw new Error("invalid-skin: pass a skin name or official: true"); + const out = await run(["use", official ? "official" : skin]); ++ if (deps.reconcile !== void 0) { ++ try { ++ await deps.reconcile(official ? null : skin); ++ } catch (error) { ++ console.error("[ui-skin-center] live reconcile invocation failed:", error); ++ } ++ } + return { + ok: true, + active: await current(), +@@ -551,7 +622,7 @@ function makeSkinCenterRoutes(deps = {}) { + /** Stable cordis plugin name (matches cordis.patch.yml insert id). */ + const name = "ui-skin-center"; + /** Services required before the skin-center can mount its routes. */ +-const inject = ["webServer"]; ++const inject = ["webServer", "loader"]; + /** + * Settings namespace for the main-interface background scrim, owned by the + * skin center. The browser half spells the same string so it can bind the +@@ -573,7 +644,7 @@ function apply(ctx) { + setSource: () => {}, + onChange: () => {} + }); +- const routes = makeSkinCenterRoutes(); ++ const routes = makeSkinCenterRoutes({ reconcile: makeLiveReconcile(ctx) }); + try { + ctx.effect(() => { + const disposers = []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 600b17756b..4f66c257cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ overrides: '@deepseek-ai/schemastery': link:vendor/schemastery patchedDependencies: + '@linxin666/dsh-client-ui-skin-center': 598def931d89aa19e734ad121ea15da57378e59950a4d96f2fd9eac008360851 node-pty@1.1.0: 7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6 importers: @@ -505,6 +506,9 @@ importers: '@deepseek-ai/dsh-host-directory-picker-native': specifier: workspace:^ version: link:../../packages/host/directory-picker-native + '@deepseek-ai/dsh-host-plugin-control': + specifier: workspace:^ + version: link:../../packages/host/plugin-control '@deepseek-ai/dsh-host-plugin-inventory': specifier: workspace:^ version: link:../../packages/host/plugin-inventory @@ -1159,6 +1163,9 @@ importers: '@deepseek-ai/dsh-goal': specifier: workspace:^ version: link:../../goal/goal + '@deepseek-ai/dsh-host-plugin-control': + specifier: workspace:^ + version: link:../../host/plugin-control '@deepseek-ai/dsh-host-plugin-inventory': specifier: workspace:^ version: link:../../host/plugin-inventory @@ -1634,6 +1641,9 @@ importers: '@deepseek-ai/dsh-client-ui-settings-models': specifier: workspace:^ version: link:../../client/ui-settings-models + '@deepseek-ai/dsh-client-ui-settings-plugin-control': + specifier: workspace:^ + version: link:../../client/ui-settings-plugin-control '@deepseek-ai/dsh-client-ui-settings-plugin-inventory': specifier: workspace:^ version: link:../../client/ui-settings-plugin-inventory @@ -1694,6 +1704,9 @@ importers: '@deepseek-ai/dsh-host-frontend-static': specifier: workspace:^ version: link:../../host/frontend-static + '@deepseek-ai/dsh-host-plugin-control': + specifier: workspace:^ + version: link:../../host/plugin-control '@deepseek-ai/dsh-host-plugin-inventory': specifier: workspace:^ version: link:../../host/plugin-inventory @@ -1730,6 +1743,45 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery + '@linxin666/dsh-client-ui-aionui-panel': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1) + '@linxin666/dsh-client-ui-git-graph': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1) + '@linxin666/dsh-client-ui-skin-center': + specifier: 0.1.2 + version: 0.1.2(patch_hash=598def931d89aa19e734ad121ea15da57378e59950a4d96f2fd9eac008360851)(react@18.3.1) + '@linxin666/dsh-client-ui-skin-whale-song': + specifier: 0.1.2 + version: 0.1.2 + '@linxin666/dsh-client-ui-task-board': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-settings@packages+settings+settings)(react@18.3.1) + '@linxin666/dsh-client-ui-web-ui-settings': + specifier: 0.1.2 + version: 0.1.2(react@18.3.1) + '@linxin666/dsh-live-stats': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-llm@packages+llm+llm)(@deepseek-ai/dsh-session-projection@packages+session+session-projection)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-token-meter@packages+llm+token-meter)(react@18.3.1) + '@linxin666/dsh-pet': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@linxin666/dsh-remote-web-ui': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@linxin666/dsh-ssh': + specifier: 0.1.2 + version: 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-tools@packages+core+tools)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@linxin666/dsh-web-ui-all': + specifier: 0.1.2 + version: 0.1.2(3dcd80cf907df19137a256ef3ea0be16) + '@omdsh-dev/dsh-annotation': + specifier: github:omdsh-dev/dsh-annotation#a37edb01c3dbb0b19249cab3b93138ad529281ed + version: https://codeload.github.com/omdsh-dev/dsh-annotation/tar.gz/a37edb01c3dbb0b19249cab3b93138ad529281ed + '@omdsh-dev/dsh-genui': + specifier: github:omdsh-dev/dsh-genui#57b4338222632f8ea81c2665d44e5f9e80b52686 + version: https://codeload.github.com/omdsh-dev/dsh-genui/tar.gz/57b4338222632f8ea81c2665d44e5f9e80b52686(@deepseek-ai/cordis@vendor+cordis)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-client-ui-tool@packages+client+ui-tool)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-llm@packages+llm+llm)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-tools@packages+core+tools)(react@18.3.1) commander: specifier: ^15.0.0 version: 15.0.0 @@ -2836,6 +2888,51 @@ importers: specifier: ^18.2.0 version: 18.3.1 + packages/client/ui-settings-plugin-control: + 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-connection': + specifier: workspace:^ + version: link:../connection + '@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-support/client-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:../../runtime-diagnostics/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-settings-plugin-inventory: devDependencies: '@deepseek-ai/cordis': @@ -5205,6 +5302,37 @@ importers: specifier: workspace:^ version: link:../../runtime-diagnostics/invariants + packages/host/plugin-control: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery + yaml: + specifier: ^2.8.2 + version: 2.9.0 + 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-atomic-write': + specifier: workspace:^ + version: link:../../util/atomic-write + '@deepseek-ai/dsh-brand': + specifier: workspace:^ + version: link:../../util/brand + '@deepseek-ai/dsh-client-connection': + specifier: workspace:^ + version: link:../../client/connection + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../runtime-diagnostics/invariants + packages/host/plugin-inventory: dependencies: zod: @@ -7933,7 +8061,7 @@ importers: version: link:../loader-smoke vitest: specifier: ^4.1.8 - version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -7982,7 +8110,7 @@ importers: 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) vitest: specifier: ^4.1.8 - version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ @@ -10070,6 +10198,12 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -10118,8 +10252,8 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + '@hono/node-server@1.19.17': + resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -10421,6 +10555,146 @@ packages: cpu: [x64] os: [win32] + '@linxin666/dsh-client-ui-aionui-panel@0.1.2': + resolution: {integrity: sha512-JPFpNpCSKtzNlqcAKh40og+TmRif2TpeC73pI2cyonmbWNu9qheoeYp2YjInknDwUYS1xyfQSInit+9rRbB3VA==} + peerDependencies: + '@deepseek-ai/dsh-client-locale': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-host-webserver': ^0.1.0-rc.6 + '@deepseek-ai/dsh-subprocess': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-workspace': ^0.1.0-rc.6 + react: ^18.2.0 + + '@linxin666/dsh-client-ui-git-graph@0.1.2': + resolution: {integrity: sha512-HRBntudoEyozHvyM9guTN1CQJ8Zaye8kuUtk5UXEIdea/Y6asbwG9b1BWaAATo/kRbKOdg+fJqjzAh60L3cn7g==} + engines: {node: ^22.19.0 || >=24.0.0} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-locale': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-conversation': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-primitives': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-host-webserver': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-subprocess': ^0.1.0-rc.6 + '@deepseek-ai/dsh-workspace': ^0.1.0-rc.6 + react: ^18.2.0 + + '@linxin666/dsh-client-ui-skin-blue-fantasy@0.1.2': + resolution: {integrity: sha512-jHc0DkAn2zhqGT7ZWGxfoUx6wvzZ/Uqcrh/gXjncZz/95xLpKxU9W89sX8Iskrv6GeD3S0vkNhtwBEfEPY6rIw==} + + '@linxin666/dsh-client-ui-skin-center@0.1.2': + resolution: {integrity: sha512-U+86t0GqbYTmuCTU0U3UJAuZZr8ub9d3OoFEiG7dq4SihPi4gGnmmrqX//MeIPkL7/9NULJMR2Q8ZctANaPeDw==} + peerDependencies: + react: ^18.2.0 + + '@linxin666/dsh-client-ui-skin-dragon-heir@0.1.2': + resolution: {integrity: sha512-vmu8Yvscr2CU3vd+lZ91vTeFSkHOeKQas9EqzwMxSJrmQTERb146On/GtsCxgB2Hp/Yc31BKysbeqHi618/G6w==} + + '@linxin666/dsh-client-ui-skin-minecraft@0.1.2': + resolution: {integrity: sha512-OZ8DOnFlhr0y+SOvhohWGQSK2ZhQWjVvFd6eWKcRPxOohnZ1aW3bliMu/wQlRW4f7uAZVsOwC9bePZ5h/9pUHg==} + + '@linxin666/dsh-client-ui-skin-qq98@0.1.2': + resolution: {integrity: sha512-i+kCDsK3VXF0u3E9Xr8iI+tyIV2DQVixcUuQuwP2GojEEuJ7bdl7qXbuSAnvjSnqLKYH7aLStTnszkTKGpco/Q==} + + '@linxin666/dsh-client-ui-skin-ths@0.1.2': + resolution: {integrity: sha512-D17sGyl09b78yJ32rdr00rDVh9z2q3XohuYbQwfHoeRJeSRNxq9oftiCZYMgfMdrbhjBGJQwcrOze4BFIrnzJw==} + + '@linxin666/dsh-client-ui-skin-whale-song@0.1.2': + resolution: {integrity: sha512-UoGMGND498vSgJ97zuLg5z+ckNZgLBE3KFhnd4OhSaRz07SaE9DCEdNhyDcbeNLdlj+JMUd51qq05VQyBFGXsQ==} + + '@linxin666/dsh-client-ui-skin-xp@0.1.2': + resolution: {integrity: sha512-r8YJ6Cfn38NzfLIiKOlQqR0O7c2s+XtiIfA2xYE7k0Yef8VzpkwrMd/Xp/64tRSfMzLzbh5k8LYdqWtBbcO2EA==} + + '@linxin666/dsh-client-ui-task-board@0.1.2': + resolution: {integrity: sha512-9KraYTF318pnZtJRL+YXpde4kvxQONYurmIG0PFnT7WaxFYBsRrxtQjrjKDR/drW5/su1SMJcwB8F0Ezw/VyAA==} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-settings': ^0.1.0-rc.6 + react: ^18.2.0 + + '@linxin666/dsh-client-ui-web-ui-settings@0.1.2': + resolution: {integrity: sha512-OSz/Ng8bBJ/tqrZH6HczoeM4WD024emFM//SdNC+Ugxi6SHB9yxbXiRMpdQa5BI0Hr51/PbyqIskeOwZvyxpQQ==} + peerDependencies: + react: ^18.2.0 + + '@linxin666/dsh-live-stats@0.1.2': + resolution: {integrity: sha512-b8Qi4Q7fzXd4KmtYusrmagDWL3RuvZC9MD088h5WTgHvmdT/ufUBptS4W53a0V8VP1c75YKmS+GGeJRkZ57N8w==} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-conversation': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session-projection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-token-meter': ^0.1.0-rc.6 + react: ^18.2.0 + + '@linxin666/dsh-pet@0.1.2': + resolution: {integrity: sha512-DhQBwvruNaJBoBBTBMQTkX8L2GRjz8W3mR0h2hPIfSEz4LZUjXIl41WYWKBWwydnS8zFjrBIlfSm1C92Bf54BQ==} + engines: {node: ^22.19.0 || >=24.0.0} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-locale': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-conversation': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-primitives': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-session': ^0.1.0-rc.6 + '@deepseek-ai/dsh-settings': ^0.1.0-rc.6 + react: ^18.2.0 + react-dom: ^18.2.0 + + '@linxin666/dsh-remote-web-ui@0.1.2': + resolution: {integrity: sha512-ieQhOY9VgJmbHnPvo0DorE+pQ+Hy0UO3PETAOPeAJH4W51kvwlqCAcKGdi9UF+MINEypoZFRX8fuhY+QSNzgug==} + engines: {node: ^22.19.0 || >=24.0.0} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-locale': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-primitives': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-sidebar': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-host-webserver': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-settings': ^0.1.0-rc.6 + react: ^18.2.0 + react-dom: ^18.2.0 + + '@linxin666/dsh-skins@0.1.2': + resolution: {integrity: sha512-L7q2TznRbpFMZG0QfcEutpa0ejJ/AZV13yureX/EbtpKYQ4y/ZxIANH2SaB1NOVCLG04Be7vVog/59mxsmmI0Q==} + + '@linxin666/dsh-ssh@0.1.2': + resolution: {integrity: sha512-1pahjEtHJnN7kvOSrOVEUDWZbGYqTyTML/UdkEws23vCU16zUjoXY8bu+T+2lr6Nnv+Hc5kGqQFRI3+Hai8z0w==} + engines: {node: ^22.19.0 || >=24.0.0} + peerDependencies: + '@deepseek-ai/dsh-client-connection': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-locale': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-sidebar': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-host-webserver': ^0.1.0-rc.6 + '@deepseek-ai/dsh-settings': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-tools': ^0.1.0-rc.6 + react: ^18.2.0 + react-dom: ^18.2.0 + + '@linxin666/dsh-web-ui-all@0.1.2': + resolution: {integrity: sha512-oSzTzKC/UW5MOCBxg39C6nYmd7TA/pysU1qp9pGd6vjDYTgO0wkkjsg+1pCS+2cXuOitOGLeDA2fJ3TQvEnsEQ==} + '@malept/cross-spawn-promise@2.0.0': resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -10470,6 +10744,13 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -10481,6 +10762,27 @@ packages: '@nodable/entities@2.2.0': resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@omdsh-dev/dsh-annotation@https://codeload.github.com/omdsh-dev/dsh-annotation/tar.gz/a37edb01c3dbb0b19249cab3b93138ad529281ed': + resolution: {gitHosted: true, integrity: sha512-euIIHPM1aBYzFobARd7FJ6201ACQVGAPRnVrwa/csoreB0oWF0hAXFV+eJTrRUpOtZ013m1v/ZZs3RAXOFjWRg==, tarball: https://codeload.github.com/omdsh-dev/dsh-annotation/tar.gz/a37edb01c3dbb0b19249cab3b93138ad529281ed} + version: 1.3.13 + engines: {node: '>=20'} + + '@omdsh-dev/dsh-genui@https://codeload.github.com/omdsh-dev/dsh-genui/tar.gz/57b4338222632f8ea81c2665d44e5f9e80b52686': + resolution: {gitHosted: true, integrity: sha512-eHdhqf/YPuQQL3LX11q6ZJLDm0XZJuzcspMKV0hUpW1Yd3vm66xXgz6cm0+k8rfWsn8M2R3kCf5/UosirZ469w==, tarball: https://codeload.github.com/omdsh-dev/dsh-genui/tar.gz/57b4338222632f8ea81c2665d44e5f9e80b52686} + version: 0.8.0 + engines: {node: ^22.19.0 || >=24.0.0, pnpm: '>=11.7.0 <12'} + peerDependencies: + '@deepseek-ai/cordis': ^4.0.1 + '@deepseek-ai/dsh-client-runtime': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-primitives': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-slots': ^0.1.0-rc.6 + '@deepseek-ai/dsh-client-ui-tool': ^0.1.0-rc.6 + '@deepseek-ai/dsh-invariants': ^0.1.0-rc.6 + '@deepseek-ai/dsh-llm': ^0.1.0-rc.6 + '@deepseek-ai/dsh-system-prompt': ^0.1.0-rc.6 + '@deepseek-ai/dsh-tools': ^0.1.0-rc.6 + react: ^18.0.0 || ^19.0.0 + '@openai/codex@0.147.0': resolution: {integrity: sha512-EQLEXecAG2ptxI7UpBMo2TR/ga5596/c/OsYF/0LoUDh5JANZ7IoGqlzBEWbuEVQ76JePIbtTW/ihCkp1a7Z3w==} engines: {node: '>=16'} @@ -11519,6 +11821,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -11705,6 +12010,9 @@ packages: '@types/node@26.1.2': resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + '@types/node@26.2.0': + resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/picomatch@3.0.2': resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} @@ -11963,6 +12271,12 @@ packages: engines: {node: '>=10.0.0'} deprecated: this version has critical issues, please update to the latest version + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + '@yarnpkg/cli-dist@4.17.1': resolution: {integrity: sha512-2tiSQuJNl/L3QwTdrq6lKWDpkcnp9MGvCT/rIldHcbu3SWfnLdmehvt3eulX1hT7FFt1Gjfq3CesF+kvhFip6g==} engines: {node: '>=18.12.0'} @@ -11986,6 +12300,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -12051,6 +12370,9 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + asn1js@3.0.10: resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} engines: {node: '>=12.0.0'} @@ -12098,6 +12420,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -12130,10 +12455,17 @@ packages: brace-expansion@2.1.2: resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + browserslist@4.28.6: resolution: {integrity: sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -12145,6 +12477,10 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + builder-util-runtime@9.7.0: resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} engines: {node: '>=12.0.0'} @@ -12242,6 +12578,10 @@ packages: clone-response@1.0.3: resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + cloudflared@0.7.3: + resolution: {integrity: sha512-dR4QMS4PbD287QqOV/vAoE1BcQCXphLOVrN9vjNUgkLrPDA5qxn9Dz0VId0P7/Czj4Mxu3hoLgbrVx5OtOWr5Q==} + hasBin: true + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -12330,6 +12670,13 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + cosmokit@1.8.1: + resolution: {integrity: sha512-PDBv4l90xZKrUsZ0vtoycgZpO/j4iFsqJXrAxsyBDsnQRI7ZMJXIjgDJsKNjd5L8jnVnnlrDCdhkFbTncgCVjQ==} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + cross-dirname@0.1.0: resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} @@ -12824,6 +13171,10 @@ packages: resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} engines: {node: '>=18.0.0'} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -12839,8 +13190,8 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express-rate-limit@8.5.2: - resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + express-rate-limit@8.6.2: + resolution: {integrity: sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -12865,8 +13216,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -12917,8 +13268,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} focus-trap@7.8.0: resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} @@ -13111,8 +13462,8 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hono@4.12.29: - resolution: {integrity: sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw==} + hono@4.13.1: + resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==} engines: {node: '>=16.9.0'} hookable@5.5.3: @@ -13201,8 +13552,8 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ip-address@10.2.0: - resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + ip-address@10.5.0: + resolution: {integrity: sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -13293,8 +13644,8 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.8: + resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -13306,6 +13657,10 @@ packages: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + jscpd-darwin-arm64@5.0.12: resolution: {integrity: sha512-CQCDYhQY9yOGGeauzkRGYW/QtXurPCjfDkEhUoM/M5UdmpzxfG3i9mnNsaheJ0RdFRf73mL7JaLVRP8U2l5/kA==} cpu: [arm64] @@ -13501,30 +13856,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -13532,6 +13917,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -13539,6 +13931,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -13546,6 +13945,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -13553,22 +13959,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -13685,8 +14114,8 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} engines: {node: '>= 0.8'} merge-descriptors@2.0.0: @@ -13816,6 +14245,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -13855,11 +14288,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -14137,6 +14578,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -14172,6 +14617,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + postject@1.0.0-alpha.6: resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} engines: {node: '>=14.0.0'} @@ -14248,6 +14697,11 @@ packages: resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} engines: {node: '>=16.0.0'} + qrcode.react@4.2.0: + resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + qs@6.15.3: resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} @@ -14430,6 +14884,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + schemastery@3.18.0: + resolution: {integrity: sha512-Jw2uxjoyyqc/yeurmChUEc/jbi8GsrdXV/KmqRUDZXJAXAmrJiPsz8vKa17l/VckyzljHZ9oGaul443CQiXxtA==} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -14573,6 +15030,10 @@ packages: sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -14780,6 +15241,9 @@ packages: resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} engines: {node: '>=18', npm: '>=9'} + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -15185,6 +15649,18 @@ packages: utf-8-validate: optional: true + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -16267,6 +16743,11 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@eslint-community/eslint-utils@4.10.1(eslint@10.5.0(jiti@2.7.0))': + dependencies: + eslint: 10.5.0(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))': dependencies: eslint: 10.5.0(jiti@2.7.0) @@ -16278,7 +16759,7 @@ snapshots: dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 10.2.6 transitivePeerDependencies: - supports-color @@ -16306,7 +16787,7 @@ snapshots: google-auth-library: 10.7.0 p-retry: 4.6.2 protobufjs: 7.6.4 - ws: 8.21.0 + ws: 8.21.3 optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) transitivePeerDependencies: @@ -16314,9 +16795,9 @@ snapshots: - supports-color - utf-8-validate - '@hono/node-server@1.19.14(hono@4.12.29)': + '@hono/node-server@1.19.17(hono@4.13.1)': dependencies: - hono: 4.12.29 + hono: 4.13.1 '@humanfs/core@0.19.2': dependencies: @@ -16533,6 +17014,185 @@ snapshots: '@koromix/koffi-win32-x64@3.1.1': optional: true + '@linxin666/dsh-client-ui-aionui-panel@0.1.2(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-locale': link:packages/client/locale + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-host-webserver': link:packages/host/webserver + '@deepseek-ai/dsh-subprocess': link:packages/subprocess/subprocess + '@deepseek-ai/dsh-system-prompt': link:packages/core/system-prompt + '@deepseek-ai/dsh-workspace': link:packages/workspace/workspace + react: 18.3.1 + + '@linxin666/dsh-client-ui-git-graph@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-locale': link:packages/client/locale + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-conversation': link:packages/client/ui-conversation + '@deepseek-ai/dsh-client-ui-primitives': link:packages/client/ui-primitives + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-host-webserver': link:packages/host/webserver + '@deepseek-ai/dsh-invariants': link:packages/runtime-diagnostics/invariants + '@deepseek-ai/dsh-subprocess': link:packages/subprocess/subprocess + '@deepseek-ai/dsh-workspace': link:packages/workspace/workspace + react: 18.3.1 + + '@linxin666/dsh-client-ui-skin-blue-fantasy@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-center@0.1.2(patch_hash=598def931d89aa19e734ad121ea15da57378e59950a4d96f2fd9eac008360851)(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@linxin666/dsh-client-ui-skin-dragon-heir@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-minecraft@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-qq98@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-ths@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-whale-song@0.1.2': {} + + '@linxin666/dsh-client-ui-skin-xp@0.1.2': {} + + '@linxin666/dsh-client-ui-task-board@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-settings@packages+settings+settings)(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-settings': link:packages/client/ui-settings + '@deepseek-ai/dsh-settings': link:packages/settings/settings + react: 18.3.1 + schemastery: 3.18.0 + + '@linxin666/dsh-client-ui-web-ui-settings@0.1.2(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@linxin666/dsh-live-stats@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-llm@packages+llm+llm)(@deepseek-ai/dsh-session-projection@packages+session+session-projection)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-token-meter@packages+llm+token-meter)(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-conversation': link:packages/client/ui-conversation + '@deepseek-ai/dsh-client-ui-settings': link:packages/client/ui-settings + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-invariants': link:packages/runtime-diagnostics/invariants + '@deepseek-ai/dsh-llm': link:packages/llm/llm + '@deepseek-ai/dsh-session': link:packages/core/session + '@deepseek-ai/dsh-session-projection': link:packages/session/session-projection + '@deepseek-ai/dsh-settings': link:packages/settings/settings + '@deepseek-ai/dsh-token-meter': link:packages/llm/token-meter + react: 18.3.1 + schemastery: 3.18.0 + zod: 4.4.3 + + '@linxin666/dsh-pet@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-locale': link:packages/client/locale + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-conversation': link:packages/client/ui-conversation + '@deepseek-ai/dsh-client-ui-primitives': link:packages/client/ui-primitives + '@deepseek-ai/dsh-client-ui-settings': link:packages/client/ui-settings + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-invariants': link:packages/runtime-diagnostics/invariants + '@deepseek-ai/dsh-session': link:packages/core/session + '@deepseek-ai/dsh-settings': link:packages/settings/settings + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schemastery: 3.18.0 + + '@linxin666/dsh-remote-web-ui@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-locale': link:packages/client/locale + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-primitives': link:packages/client/ui-primitives + '@deepseek-ai/dsh-client-ui-settings': link:packages/client/ui-settings + '@deepseek-ai/dsh-client-ui-sidebar': link:packages/client/ui-sidebar + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-host-webserver': link:packages/host/webserver + '@deepseek-ai/dsh-invariants': link:packages/runtime-diagnostics/invariants + '@deepseek-ai/dsh-settings': link:packages/settings/settings + cloudflared: 0.7.3 + clsx: 2.1.1 + qrcode.react: 4.2.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schemastery: 3.18.0 + zod: 4.4.3 + + '@linxin666/dsh-skins@0.1.2(react@18.3.1)': + dependencies: + '@linxin666/dsh-client-ui-skin-blue-fantasy': 0.1.2 + '@linxin666/dsh-client-ui-skin-center': 0.1.2(patch_hash=598def931d89aa19e734ad121ea15da57378e59950a4d96f2fd9eac008360851)(react@18.3.1) + '@linxin666/dsh-client-ui-skin-dragon-heir': 0.1.2 + '@linxin666/dsh-client-ui-skin-minecraft': 0.1.2 + '@linxin666/dsh-client-ui-skin-qq98': 0.1.2 + '@linxin666/dsh-client-ui-skin-ths': 0.1.2 + '@linxin666/dsh-client-ui-skin-xp': 0.1.2 + transitivePeerDependencies: + - react + + '@linxin666/dsh-ssh@0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-tools@packages+core+tools)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@deepseek-ai/dsh-client-connection': link:packages/client/connection + '@deepseek-ai/dsh-client-locale': link:packages/client/locale + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-settings': link:packages/client/ui-settings + '@deepseek-ai/dsh-client-ui-sidebar': link:packages/client/ui-sidebar + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-host-webserver': link:packages/host/webserver + '@deepseek-ai/dsh-settings': link:packages/settings/settings + '@deepseek-ai/dsh-system-prompt': link:packages/core/system-prompt + '@deepseek-ai/dsh-tools': link:packages/core/tools + '@xterm/addon-fit': 0.11.0 + '@xterm/xterm': 6.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + ssh2: 1.17.0 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@linxin666/dsh-web-ui-all@0.1.2(3dcd80cf907df19137a256ef3ea0be16)': + dependencies: + '@linxin666/dsh-client-ui-aionui-panel': 0.1.2(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1) + '@linxin666/dsh-client-ui-git-graph': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-subprocess@packages+subprocess+subprocess)(@deepseek-ai/dsh-workspace@packages+workspace+workspace)(react@18.3.1) + '@linxin666/dsh-client-ui-task-board': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-settings@packages+settings+settings)(react@18.3.1) + '@linxin666/dsh-client-ui-web-ui-settings': 0.1.2(react@18.3.1) + '@linxin666/dsh-live-stats': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-llm@packages+llm+llm)(@deepseek-ai/dsh-session-projection@packages+session+session-projection)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-token-meter@packages+llm+token-meter)(react@18.3.1) + '@linxin666/dsh-pet': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-conversation@packages+client+ui-conversation)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-session@packages+core+session)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@linxin666/dsh-remote-web-ui': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-settings@packages+settings+settings)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@linxin666/dsh-skins': 0.1.2(react@18.3.1) + '@linxin666/dsh-ssh': 0.1.2(@deepseek-ai/dsh-client-connection@packages+client+connection)(@deepseek-ai/dsh-client-locale@packages+client+locale)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-settings@packages+client+ui-settings)(@deepseek-ai/dsh-client-ui-sidebar@packages+client+ui-sidebar)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-host-webserver@packages+host+webserver)(@deepseek-ai/dsh-settings@packages+settings+settings)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-tools@packages+core+tools)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@deepseek-ai/dsh-client-connection' + - '@deepseek-ai/dsh-client-locale' + - '@deepseek-ai/dsh-client-runtime' + - '@deepseek-ai/dsh-client-ui-conversation' + - '@deepseek-ai/dsh-client-ui-primitives' + - '@deepseek-ai/dsh-client-ui-settings' + - '@deepseek-ai/dsh-client-ui-sidebar' + - '@deepseek-ai/dsh-client-ui-slots' + - '@deepseek-ai/dsh-host-webserver' + - '@deepseek-ai/dsh-invariants' + - '@deepseek-ai/dsh-llm' + - '@deepseek-ai/dsh-session' + - '@deepseek-ai/dsh-session-projection' + - '@deepseek-ai/dsh-settings' + - '@deepseek-ai/dsh-subprocess' + - '@deepseek-ai/dsh-system-prompt' + - '@deepseek-ai/dsh-token-meter' + - '@deepseek-ai/dsh-tools' + - '@deepseek-ai/dsh-workspace' + - bufferutil + - react + - react-dom + - utf-8-validate + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 @@ -16564,7 +17224,7 @@ snapshots: '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/semantic-conventions': 1.43.0 - ws: 8.21.0 + ws: 8.21.3 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) optionalDependencies: @@ -16577,18 +17237,18 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.29) + '@hono/node-server': 1.19.17(hono@4.13.1) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 eventsource: 3.0.7 - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 express: 5.2.1 - express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.29 - jose: 6.2.3 + express-rate-limit: 8.6.2(express@5.2.1) + hono: 4.13.1 + jose: 6.2.8 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 @@ -16633,12 +17293,34 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + '@noble/hashes@1.4.0': {} '@noble/hashes@2.3.0': {} '@nodable/entities@2.2.0': {} + '@omdsh-dev/dsh-annotation@https://codeload.github.com/omdsh-dev/dsh-annotation/tar.gz/a37edb01c3dbb0b19249cab3b93138ad529281ed': {} + + '@omdsh-dev/dsh-genui@https://codeload.github.com/omdsh-dev/dsh-genui/tar.gz/57b4338222632f8ea81c2665d44e5f9e80b52686(@deepseek-ai/cordis@vendor+cordis)(@deepseek-ai/dsh-client-runtime@packages+client+runtime)(@deepseek-ai/dsh-client-ui-primitives@packages+client+ui-primitives)(@deepseek-ai/dsh-client-ui-slots@packages+client+ui-slots)(@deepseek-ai/dsh-client-ui-tool@packages+client+ui-tool)(@deepseek-ai/dsh-invariants@packages+runtime-diagnostics+invariants)(@deepseek-ai/dsh-llm@packages+llm+llm)(@deepseek-ai/dsh-system-prompt@packages+core+system-prompt)(@deepseek-ai/dsh-tools@packages+core+tools)(react@18.3.1)': + dependencies: + '@deepseek-ai/cordis': link:vendor/cordis + '@deepseek-ai/dsh-client-runtime': link:packages/client/runtime + '@deepseek-ai/dsh-client-ui-primitives': link:packages/client/ui-primitives + '@deepseek-ai/dsh-client-ui-slots': link:packages/client/ui-slots + '@deepseek-ai/dsh-client-ui-tool': link:packages/client/ui-tool + '@deepseek-ai/dsh-invariants': link:packages/runtime-diagnostics/invariants + '@deepseek-ai/dsh-llm': link:packages/llm/llm + '@deepseek-ai/dsh-system-prompt': link:packages/core/system-prompt + '@deepseek-ai/dsh-tools': link:packages/core/tools + react: 18.3.1 + '@openai/codex@0.147.0': optionalDependencies: '@openai/codex-darwin-arm64': '@openai/codex@0.147.0-darwin-arm64' @@ -16864,7 +17546,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@oxc-resolver/binding-win32-arm64-msvc@11.20.0': @@ -17075,7 +17757,7 @@ snapshots: dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true '@rolldown/binding-wasm32-wasi@1.1.1': @@ -17364,6 +18046,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@5.0.4': {} '@types/babel__code-frame@7.27.0': {} @@ -17590,6 +18277,11 @@ snapshots: dependencies: undici-types: 8.3.0 + '@types/node@26.2.0': + dependencies: + undici-types: 8.3.0 + optional: true + '@types/picomatch@3.0.2': {} '@types/prop-types@15.7.15': {} @@ -17691,13 +18383,13 @@ snapshots: optionalDependencies: vite: 8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -17875,6 +18567,10 @@ snapshots: '@xmldom/xmldom@0.8.13': {} + '@xterm/addon-fit@0.11.0': {} + + '@xterm/xterm@6.0.0': {} + '@yarnpkg/cli-dist@4.17.1': {} abbrev@4.0.0: {} @@ -17888,8 +18584,14 @@ snapshots: dependencies: acorn: 8.17.0 + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + acorn@8.17.0: {} + acorn@8.18.0: {} + agent-base@7.1.4: {} ajv-formats@3.0.1(ajv@8.20.0): @@ -17906,7 +18608,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.5 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -17976,10 +18678,10 @@ snapshots: hosted-git-info: 4.1.0 isbinaryfile: 5.0.7 jiti: 2.7.0 - js-yaml: 4.2.0 + js-yaml: 4.3.1 json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 10.2.5 + minimatch: 10.2.6 pkijs: 3.4.0 plist: 3.1.0 proper-lockfile: 4.1.2 @@ -17999,6 +18701,10 @@ snapshots: dependencies: dequal: 2.0.3 + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + asn1js@3.0.10: dependencies: pvtsutils: 1.3.6 @@ -18037,6 +18743,10 @@ snapshots: baseline-browser-mapping@2.10.43: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -18077,10 +18787,18 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 @@ -18093,6 +18811,9 @@ snapshots: buffer-from@1.1.2: {} + buildcheck@0.0.7: + optional: true + builder-util-runtime@9.7.0: dependencies: debug: 4.4.3 @@ -18110,7 +18831,7 @@ snapshots: fs-extra: 10.1.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - js-yaml: 4.2.0 + js-yaml: 4.3.1 sanitize-filename: 1.6.4 source-map-support: 0.5.21 stat-mode: 1.0.0 @@ -18194,6 +18915,8 @@ snapshots: dependencies: mimic-response: 1.0.1 + cloudflared@0.7.3: {} + clsx@2.1.1: {} color-convert@2.0.1: @@ -18256,6 +18979,14 @@ snapshots: dependencies: layout-base: 2.0.1 + cosmokit@1.8.1: {} + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + cross-dirname@0.1.0: optional: true @@ -18534,7 +19265,7 @@ snapshots: app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) builder-util: 26.15.3 fs-extra: 10.1.0 - js-yaml: 4.2.0 + js-yaml: 4.3.1 transitivePeerDependencies: - electron-builder-squirrel-windows - supports-color @@ -18827,7 +19558,7 @@ snapshots: eslint@10.5.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.5.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -18854,7 +19585,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -18870,8 +19601,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esquery@1.7.0: @@ -18896,9 +19627,11 @@ snapshots: eventsource-parser@3.1.0: {} + eventsource-parser@3.1.1: {} + eventsource@3.0.7: dependencies: - eventsource-parser: 3.1.0 + eventsource-parser: 3.1.1 execa@10.0.0: dependencies: @@ -18920,10 +19653,13 @@ snapshots: exponential-backoff@3.1.3: {} - express-rate-limit@8.5.2(express@5.2.1): + express-rate-limit@8.6.2(express@5.2.1): dependencies: + debug: 4.4.3 express: 5.2.1 - ip-address: 10.2.0 + ip-address: 10.5.0 + transitivePeerDependencies: + - supports-color express@5.2.1: dependencies: @@ -18970,7 +19706,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-uri@3.1.3: {} + fast-uri@3.1.5: {} fast-xml-builder@1.2.0: dependencies: @@ -19029,10 +19765,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.4 keyv: 4.5.4 - flatted@3.4.2: {} + flatted@3.4.4: {} focus-trap@7.8.0: dependencies: @@ -19286,7 +20022,7 @@ snapshots: dependencies: '@types/hast': 3.0.5 - hono@4.12.29: {} + hono@4.13.1: {} hookable@5.5.3: {} @@ -19368,7 +20104,7 @@ snapshots: internmap@2.0.3: {} - ip-address@10.2.0: {} + ip-address@10.5.0: {} ipaddr.js@1.9.1: {} @@ -19435,7 +20171,7 @@ snapshots: jiti@2.7.0: {} - jose@6.2.3: {} + jose@6.2.8: {} js-tokens@10.0.0: {} @@ -19445,6 +20181,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + jscpd-darwin-arm64@5.0.12: optional: true @@ -19659,36 +20399,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -19705,6 +20478,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -19894,7 +20683,7 @@ snapshots: mdn-data@2.27.1: {} - media-typer@1.1.0: {} + media-typer@1.1.1: {} merge-descriptors@2.0.0: {} @@ -20145,13 +20934,17 @@ snapshots: dependencies: brace-expansion: 5.0.6 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.18 minimatch@5.1.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.4 minimatch@9.0.9: dependencies: @@ -20177,8 +20970,13 @@ snapshots: ms@2.1.3: {} + nan@2.28.0: + optional: true + nanoid@3.3.12: {} + nanoid@3.3.18: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} @@ -20480,6 +21278,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pkce-challenge@5.0.1: {} pkijs@3.4.0: @@ -20520,6 +21320,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 @@ -20599,6 +21405,10 @@ snapshots: pvutils@1.2.0: {} + qrcode.react@4.2.0(react@18.3.1): + dependencies: + react: 18.3.1 + qs@6.15.3: dependencies: es-define-property: 1.0.1 @@ -20840,6 +21650,11 @@ snapshots: dependencies: loose-envify: 1.4.0 + schemastery@3.18.0: + dependencies: + '@standard-schema/spec': 1.1.0 + cosmokit: 1.8.1 + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -21023,6 +21838,14 @@ snapshots: sprintf-js@1.1.3: optional: true + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.28.0 + stackback@0.0.2: {} stat-mode@1.0.0: {} @@ -21204,6 +22027,8 @@ snapshots: dependencies: '@mixmark-io/domino': 2.2.0 + tweetnacl@0.14.5: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -21214,7 +22039,7 @@ snapshots: type-is@2.1.0: dependencies: content-type: 2.0.0 - media-typer: 1.1.0 + media-typer: 1.1.1 mime-types: 3.0.2 typebox@1.1.38: {} @@ -21359,9 +22184,9 @@ snapshots: vite@8.0.16(@types/node@22.20.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.26 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -21372,15 +22197,15 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.15 + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.26 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.2 + '@types/node': 26.2.0 esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 @@ -21474,10 +22299,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -21494,7 +22319,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -21534,10 +22359,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@2.3.0))(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -21554,7 +22379,7 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@26.1.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vite: 8.0.16(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -21652,6 +22477,8 @@ snapshots: ws@8.21.0: {} + ws@8.21.3: {} + xml-name-validator@5.0.0: {} xml-naming@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 68444953d5..07d7a6fcec 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,6 +58,13 @@ allowBuilds: # The Python runtime deploy includes the reviewed workspace postinstall that # restores the executable bit on node-pty's macOS spawn helper. '@deepseek-ai/dsh-subprocess-local@file:packages/subprocess/subprocess-local': true + # remote-web-ui installs cloudflared lazily when a quick tunnel is first + # requested; its dependency postinstall download is unnecessary. + cloudflared: false + # ssh2 has complete JavaScript implementations; these scripts only compile + # optional CPU-detection and crypto accelerators. + cpu-features: false + ssh2: false minimumReleaseAgeExclude: # Fresh pi-ai releases carry the model catalog updates that are the whole @@ -72,6 +79,27 @@ minimumReleaseAgeExclude: - node-addon-require-builtin-win32-ia32-msvc@0.1.4 - node-addon-require-builtin-win32-x64-msvc@0.1.4 - node-addon-require-builtin@0.1.4 + # The requested dsh-web-ui 0.1.2 bundle was published inside the ordinary + # release-age window; every aggregate member is pinned in the lockfile. + - '@linxin666/dsh-client-ui-aionui-panel@0.1.2' + - '@linxin666/dsh-client-ui-git-graph@0.1.2' + - '@linxin666/dsh-client-ui-skin-blue-fantasy@0.1.2' + - '@linxin666/dsh-client-ui-skin-center@0.1.2' + - '@linxin666/dsh-client-ui-skin-dragon-heir@0.1.2' + - '@linxin666/dsh-client-ui-skin-minecraft@0.1.2' + - '@linxin666/dsh-client-ui-skin-qq98@0.1.2' + - '@linxin666/dsh-client-ui-skin-ths@0.1.2' + - '@linxin666/dsh-client-ui-skin-xp@0.1.2' + - '@linxin666/dsh-client-ui-task-board@0.1.2' + - '@linxin666/dsh-client-ui-web-ui-settings@0.1.2' + - '@linxin666/dsh-live-stats@0.1.2' + - '@linxin666/dsh-pet@0.1.2' + - '@linxin666/dsh-remote-web-ui@0.1.2' + - '@linxin666/dsh-skins@0.1.2' + - '@linxin666/dsh-ssh@0.1.2' + - '@linxin666/dsh-web-ui-all@0.1.2' + - '@linxin666/dsh-client-ui-skin-whale-song@0.1.2' patchedDependencies: + '@linxin666/dsh-client-ui-skin-center': patches/@linxin666__dsh-client-ui-skin-center.patch node-pty@1.1.0: patches/node-pty@1.1.0.patch diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index ed622a12ee..5f85974fc2 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -133,6 +133,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record = { configuredAgentIdentities: 'not a service: launcher-provided boot-context value (ConfiguredAgentIdentities | undefined) — packages/core/agent-loop/README.md owns this launcher contract', launcherSessionQueryPath: 'not a service: launcher-provided boot-context value (string | undefined) — packages/session-query/session-query-sqlite/README.md owns this launcher contract', dshHomePath: 'not a service: boot-provided root accessor function (typeof dshHomePath | undefined) for Loader !!js config expressions — packages/boot/app-boot/README.md owns the boot contract', + profileUserPatchPath: 'not a service: launcher-provided active-profile patch path (string | undefined) for trusted Host plugins — packages/boot/app-boot/README.md owns the boot contract', launchEnvironment: 'not a service: launcher-provided root accessor value (LaunchEnvironmentSnapshot | undefined) — packages/util/launch-environment/README.md owns this launcher contract', connection: 'interface-typed (HostConnectionHandle); implementing class HostConnectionService is declared in rpc-host.ts — packages/client/connection/README.md owns the API', appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the API', diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts index 47b2d2c9e8..88865653e2 100644 --- a/scripts/gen-third-party-notices.ts +++ b/scripts/gen-third-party-notices.ts @@ -77,6 +77,7 @@ const OVERRIDES: Record = { '@modelcontextprotocol/server-everything': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' }, '@modelcontextprotocol/server-filesystem': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' }, // No repository field in the published manifest. + '@linxin666/dsh-web-ui-all': { repo: 'https://github.com/zhu1090093659/dsh-web-ui' }, 'node-addon-require-builtin': { repo: 'https://www.npmjs.com/package/node-addon-require-builtin' }, } diff --git a/scripts/link-community-skins.mjs b/scripts/link-community-skins.mjs new file mode 100644 index 0000000000..55149b5950 --- /dev/null +++ b/scripts/link-community-skins.mjs @@ -0,0 +1,142 @@ +/** + * Maintain the deployment-owned skins tree the bundled skin center reads: + * `/node_modules/skins/` links to the installed + * `@linxin666/dsh-client-ui-skin-` package directories. The patched + * `@linxin666/dsh-client-ui-skin-center` walks ancestors of its own location + * for a `skins/` directory, so this canonical workspace-level tree serves + * source launches (`pnpm dsh web`) without touching pnpm's virtual store. + * The desktop packaging assembles the same tree into the app bundle at + * stage time (apps/desktop/scripts/prepare-package.mjs), so this script is + * never used by the packaged product. + * + * Idempotent: correct links are kept, wrong or dangling links are replaced, + * and a real directory in the way fails loudly. Missing packages (partial + * installs) are skipped silently so a fresh checkout's install never fails. + * @module scripts/link-community-skins + */ + +import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, symlinkSync, unlinkSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' +import { createRequire } from 'node:module' +import { fileURLToPath } from 'node:url' + +/** The web bundle owns the community plugin roster; its manifest names the skin packages. */ +const BUNDLE_ANCHOR = resolve(import.meta.dirname, '..', 'packages/bundle/web-app/package.json') + +/** Package-name prefix of every skin in the shipped community set. */ +const SKIN_PACKAGE_PREFIX = '@linxin666/dsh-client-ui-skin-' + +/** The skins aggregate whose dependency list carries the bundle-wired skins. */ +const SKINS_AGGREGATE = '@linxin666/dsh-skins' + +/** The web-ui aggregate whose dependency closure carries the skins aggregate. */ +const WEB_UI_AGGREGATE = '@linxin666/dsh-web-ui-all' + +/** Maximum parent hops while probing a package directory from an anchor. */ +const PROBE_HOP_LIMIT = 16 + +/** + * Find a package's directory from an anchor manifest the way Node resolves + * imports: probe `node_modules` parents without requiring the package to + * export `./package.json`. + * @param anchor - absolute path of a manifest file inside the anchor package. + * @param packageName - the package name to resolve. + * @returns the real package directory, or `undefined` when not installed. + */ +function packageDirFromAnchor(anchor, packageName) { + const require = createRequire(anchor) + for (const searchPath of require.resolve.paths(packageName) ?? []) { + let current = searchPath + for (let hop = 0; hop < PROBE_HOP_LIMIT; hop += 1) { + const candidate = join(current, packageName) + if (existsSync(join(candidate, 'package.json'))) { + return realpathSync(candidate) + } + current = dirname(current) + } + } + return undefined +} + +/** Read one manifest's dependencies object. */ +function readDependencies(manifestPath) { + const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) + const dependencies = parsed.dependencies + if (typeof dependencies !== 'object' || dependencies === null) return {} + return dependencies +} + +/** + * Replace `link` with a symlink to `target`, or keep a correct link. + * @param link - absolute link path. + * @param target - absolute target path. + */ +function ensureLink(link, target) { + let stat + try { + stat = lstatSync(link) + } catch { + stat = undefined + } + if (stat !== undefined) { + if (!stat.isSymbolicLink()) { + throw new Error(`link-community-skins: ${link} exists and is not a symlink; remove it so the skins tree can be maintained`) + } + if (readlinkSync(link) === target) return + unlinkSync(link) + } + mkdirSync(dirname(link), { recursive: true }) + symlinkSync(target, link, 'junction') +} + +/** Resolve the shipped skin packages: aggregate chains plus the web bundle's own extras. */ +function resolveSkinPackages() { + const pending = new Map() + for (const [name, range] of Object.entries(readDependencies(BUNDLE_ANCHOR))) { + if (name.startsWith(SKIN_PACKAGE_PREFIX)) pending.set(name, BUNDLE_ANCHOR) + } + // The wired skins are nested dependencies: they resolve from the web-ui + // aggregate's own node_modules, and the skins aggregate's manifest carries + // their names. Each resolution anchors at the manifest that declares them. + const webUiAggregateDir = packageDirFromAnchor(BUNDLE_ANCHOR, WEB_UI_AGGREGATE) + if (webUiAggregateDir !== undefined) { + const webUiManifest = join(webUiAggregateDir, 'package.json') + const skinsAggregateDir = packageDirFromAnchor(webUiManifest, SKINS_AGGREGATE) + if (skinsAggregateDir !== undefined) { + const skinsManifest = join(skinsAggregateDir, 'package.json') + for (const name of Object.keys(readDependencies(skinsManifest))) { + if (name.startsWith(SKIN_PACKAGE_PREFIX)) pending.set(name, skinsManifest) + } + } + } + const skins = new Map() + for (const [name, anchor] of pending) { + const dir = packageDirFromAnchor(anchor, name) + // The skin-center plugin itself shares the package-name prefix but is not + // a skin; only packages carrying a skin.json belong in the skins tree. + if (dir !== undefined && existsSync(join(dir, 'skin.json'))) { + skins.set(name.slice(SKIN_PACKAGE_PREFIX.length), dir) + } + } + return skins +} + +/** Assemble `/node_modules/skins/` links and report changes. */ +export function linkCommunitySkins(workspaceRoot = resolve(import.meta.dirname, '..')) { + const skinsRoot = join(workspaceRoot, 'node_modules', 'skins') + const skins = resolveSkinPackages() + let created = 0 + for (const [id, target] of skins) { + ensureLink(join(skinsRoot, id), target) + created += 1 + } + return { skinsRoot, count: created } +} + +// Standalone execution: postinstall hook. +if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { + const result = linkCommunitySkins() + if (result.count > 0) { + console.log(`link-community-skins: ${result.count} skin link(s) ready at ${result.skinsRoot}`) + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index bdaaa175f0..51dcb36bed 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -170,6 +170,8 @@ "@deepseek-ai/dsh-host-apiproxy/*": ["./packages/host/apiproxy/src/*"], "@deepseek-ai/dsh-host-webserver": ["./packages/host/webserver/src"], "@deepseek-ai/dsh-host-frontend-static": ["./packages/host/frontend-static/src"], + "@deepseek-ai/dsh-host-plugin-control": ["./packages/host/plugin-control/src"], + "@deepseek-ai/dsh-host-plugin-control/types": ["./packages/host/plugin-control/src/types.ts"], "@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"], @@ -218,6 +220,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-settings-models": ["./packages/client/ui-settings-models/src"], + "@deepseek-ai/dsh-client-ui-settings-plugin-control": ["./packages/client/ui-settings-plugin-control/src"], "@deepseek-ai/dsh-client-ui-settings-plugin-inventory": ["./packages/client/ui-settings-plugin-inventory/src"], "@deepseek-ai/dsh-client-locale": ["./packages/client/locale/src"], "@deepseek-ai/dsh-client-web": ["./packages/client/web/src"], diff --git a/tsconfig.client.json b/tsconfig.client.json index e599f76f1b..f40279b444 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -90,6 +90,7 @@ { "path": "./packages/client/ui-settings" }, { "path": "./packages/client/ui-settings-general" }, { "path": "./packages/client/ui-settings-models" }, + { "path": "./packages/client/ui-settings-plugin-control" }, { "path": "./packages/client/ui-settings-plugin-inventory" }, { "path": "./packages/client/locale" }, { "path": "./packages/client/web" }, diff --git a/tsconfig.host.json b/tsconfig.host.json index 8ee5247631..6c9d9e4bbb 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -287,6 +287,7 @@ { "path": "./packages/host/directory-picker-browse" }, { "path": "./packages/host/directory-picker-native" }, { "path": "./packages/host/frontend-static" }, + { "path": "./packages/host/plugin-control" }, { "path": "./packages/host/plugin-inventory" }, { "path": "./packages/host/webserver" }, { "path": "./packages/sdk/client" }, From 577ec96e19a26429cb867884b4f647308d642c40 Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 14 Aug 2026 03:41:31 +0800 Subject: [PATCH 2/3] ci: skip upstream issue-policy workflows on forks without their credentials Issue lifecycle needs the DSH GitHub App credentials and Issue policy reads its repository identity from the upstream-owned config; both are absent on forks, so the jobs fail on every pull_request event. Guard them to skip cleanly outside the upstream repository. --- .github/workflows/issue-lifecycle.yml | 4 +++- .github/workflows/issue-policy.yml | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-lifecycle.yml b/.github/workflows/issue-lifecycle.yml index e324cfefc2..c899350fb7 100644 --- a/.github/workflows/issue-lifecycle.yml +++ b/.github/workflows/issue-lifecycle.yml @@ -36,7 +36,9 @@ concurrency: jobs: lifecycle: name: Issue lifecycle - if: ${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }} + # The project-token App credentials exist only in the upstream repository; + # forks without them skip cleanly instead of failing on every event. + if: ${{ vars.DSH_ISSUE_APP_CLIENT_ID != '' && secrets.DSH_ISSUE_APP_PRIVATE_KEY != '' && (github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested')) }} runs-on: ubuntu-latest steps: - name: Check out trusted policy diff --git a/.github/workflows/issue-policy.yml b/.github/workflows/issue-policy.yml index dde9462c33..326f02cfc0 100644 --- a/.github/workflows/issue-policy.yml +++ b/.github/workflows/issue-policy.yml @@ -14,6 +14,10 @@ permissions: jobs: policy: name: Issue policy + # The policy script reads its repository identity from the upstream-owned + # config (project board, labels, lifecycle actor); forks cannot run it + # meaningfully, so skip cleanly instead of failing on the hardcoded repo. + if: ${{ github.repository == 'deepseek-harness/deepseek-harness' }} runs-on: ubuntu-latest steps: - name: Check out trusted policy From 4a837c737ae4ae6e517e74cfbbe8eab9b5f9d563 Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 14 Aug 2026 03:45:03 +0800 Subject: [PATCH 3/3] ci: guard issue-lifecycle credentials at step level Job-level if cannot read the secrets context, so the fork guard failed workflow evaluation instead of skipping. Move the credential check onto the token step and gate the handler on the minted token, which lets forks complete the job with all steps skipped. --- .github/workflows/issue-lifecycle.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-lifecycle.yml b/.github/workflows/issue-lifecycle.yml index c899350fb7..f0bb2b189d 100644 --- a/.github/workflows/issue-lifecycle.yml +++ b/.github/workflows/issue-lifecycle.yml @@ -36,9 +36,7 @@ concurrency: jobs: lifecycle: name: Issue lifecycle - # The project-token App credentials exist only in the upstream repository; - # forks without them skip cleanly instead of failing on every event. - if: ${{ vars.DSH_ISSUE_APP_CLIENT_ID != '' && secrets.DSH_ISSUE_APP_PRIVATE_KEY != '' && (github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested')) }} + if: ${{ github.event_name != 'pull_request_review' || (github.event.action == 'submitted' && github.event.review.state == 'changes_requested') }} runs-on: ubuntu-latest steps: - name: Check out trusted policy @@ -46,8 +44,12 @@ jobs: with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false + # The project-token App credentials exist only in the upstream + # repository; forks without them skip the token and handler steps + # cleanly (job-level `if` cannot read the secrets context). - name: Create project token id: app-token + if: ${{ vars.DSH_ISSUE_APP_CLIENT_ID != '' && secrets.DSH_ISSUE_APP_PRIVATE_KEY != '' }} uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 with: client-id: ${{ vars.DSH_ISSUE_APP_CLIENT_ID }} @@ -55,6 +57,7 @@ jobs: owner: deepseek-harness repositories: deepseek-harness - name: Handle repository event + if: ${{ steps.app-token.outputs.token != '' }} env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: node .github/issue-management/policy.mjs lifecycle