Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,也不依赖激活顺序。
Expand All @@ -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。
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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/<id>`), 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/<id>` 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: <control-id>` 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.
Loading
Loading