Przeglądaj źródła

refactor(gui): features register their own settings surfaces

Settings collaboration direction (recorded in the note): the shell only
provides composition faces — feature plugins register themselves. The
General section moves into the ui-settings shell (order 0, skeleton
rows) and declares the settings.general.item list slot; locale registers
the Language row and ui-theme the Appearance row (each with its own
store mirror, dictionaries, and ledger-judged deferral); the
ui-settings-general package is gone. ui-settings-models becomes
ui-models — a feature package that contributes its Settings section
rather than a settings-owned satellite. The item-slot SlotMap entry is
authored in the ui-settings contract and repeated verbatim in
locale/ui-theme (reference-cycle avoidance; declaration merging keeps
the copies identical).
imccyu 2 miesięcy temu
rodzic
commit
23a60ade67
62 zmienionych plików z 995 dodań i 1036 usunięć
  1. 26 16
      .agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md
  2. 27 17
      .agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.zh.md
  3. 2 5
      apps/cli/cordis.yml
  4. 1 2
      apps/cli/package.json
  5. 1 4
      apps/cli/tsconfig.json
  6. 1 2
      apps/web/tests/session-title.snapshot.ts
  7. 1 2
      apps/web/tests/workspace-flow.snapshot.ts
  8. 18 4
      packages/client/locale/package.json
  9. 47 0
      packages/client/locale/src/client/LanguageRow.module.css
  10. 68 0
      packages/client/locale/src/client/LanguageRow.tsx
  11. 67 5
      packages/client/locale/src/client/index.ts
  12. 18 0
      packages/client/locale/src/client/settings-contract.ts
  13. 47 0
      packages/client/locale/src/client/settings-store.ts
  14. 0 0
      packages/client/locale/src/css-modules.d.ts
  15. 5 1
      packages/client/locale/tests/invariant.spec.ts
  16. 9 0
      packages/client/locale/tsconfig.json
  17. 1 1
      packages/client/ui-models/README.md
  18. 2 2
      packages/client/ui-models/package.json
  19. 0 0
      packages/client/ui-models/src/client/ModelsSection.tsx
  20. 2 2
      packages/client/ui-models/src/client/index.ts
  21. 0 0
      packages/client/ui-models/src/css-modules.d.ts
  22. 0 0
      packages/client/ui-models/src/index.ts
  23. 4 4
      packages/client/ui-models/src/invariant.ts
  24. 2 2
      packages/client/ui-models/tests/apply.spec.ts
  25. 2 2
      packages/client/ui-models/tests/invariant.spec.ts
  26. 0 0
      packages/client/ui-models/tsconfig.json
  27. 3 0
      packages/client/ui-models/tsdown.config.ts
  28. 0 15
      packages/client/ui-settings-general/README.md
  29. 0 70
      packages/client/ui-settings-general/package.json
  30. 0 118
      packages/client/ui-settings-general/src/client/GeneralSection.tsx
  31. 0 66
      packages/client/ui-settings-general/src/client/contract.ts
  32. 0 117
      packages/client/ui-settings-general/src/client/index.ts
  33. 0 43
      packages/client/ui-settings-general/src/client/store.ts
  34. 0 4
      packages/client/ui-settings-general/src/index.ts
  35. 0 32
      packages/client/ui-settings-general/src/invariant.ts
  36. 0 139
      packages/client/ui-settings-general/tests/apply.spec.ts
  37. 0 112
      packages/client/ui-settings-general/tests/general-section.spec.tsx
  38. 0 18
      packages/client/ui-settings-general/tests/invariant.spec.ts
  39. 0 56
      packages/client/ui-settings-general/tests/store.spec.ts
  40. 0 36
      packages/client/ui-settings-general/tsconfig.json
  41. 0 3
      packages/client/ui-settings-general/tsdown.config.ts
  42. 0 3
      packages/client/ui-settings-models/tsdown.config.ts
  43. 9 29
      packages/client/ui-settings/src/client/GeneralSection.module.css
  44. 51 0
      packages/client/ui-settings/src/client/GeneralSection.tsx
  45. 35 3
      packages/client/ui-settings/src/client/contract/slots.ts
  46. 51 8
      packages/client/ui-settings/src/client/index.ts
  47. 12 14
      packages/client/ui-settings/src/client/locales.ts
  48. 77 4
      packages/client/ui-settings/tests/apply.spec.ts
  49. 47 0
      packages/client/ui-settings/tests/general-section.spec.tsx
  50. 21 4
      packages/client/ui-theme/package.json
  51. 51 0
      packages/client/ui-theme/src/client/AppearanceRow.module.css
  52. 63 0
      packages/client/ui-theme/src/client/AppearanceRow.tsx
  53. 83 6
      packages/client/ui-theme/src/client/index.ts
  54. 17 0
      packages/client/ui-theme/src/client/settings-contract.ts
  55. 37 0
      packages/client/ui-theme/src/client/settings-store.ts
  56. 6 0
      packages/client/ui-theme/src/css-modules.d.ts
  57. 8 2
      packages/client/ui-theme/tests/invariant.spec.ts
  58. 12 0
      packages/client/ui-theme/tsconfig.json
  59. 58 57
      pnpm-lock.yaml
  60. 1 2
      scripts/verify-package-readme-model-experience.ts
  61. 1 2
      tsconfig.base.json
  62. 1 2
      tsconfig.client.json

+ 26 - 16
.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md

@@ -10,41 +10,48 @@ The browser client's existing Settings is written directly inside the Sidebar, a
 
 ## Proposal
 
-The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares the `settings.section` list slot. Each section is contributed by an independent plugin; the Settings shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`.
+**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell provides only the composition surface (the top-level section list plus the item list inside General) and neither imports nor enumerates any feature; for a feature to appear in Settings, its own plugin registers into the corresponding slot — locale registers the Language row, ui-theme registers the Appearance row, ui-models registers the Models top-level panel. No separate `ui-settings-*` package is created for "a feature's settings page": the settings surface belongs to the feature package itself (shipping the Theme feature means Theme's settings choices ship with ui-theme). The only content the shell carries itself is the first top-level directory, General (skeleton rows plus the item slot declaration), because it belongs to no single feature.
+
+The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares the `settings.section` list slot. Each section is contributed by a feature plugin; the Settings shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`. General is registered by the shell itself (order 0) and declares the `settings.general.item` list slot, into which the feature plugins' preference rows slot by order.
 
 The Settings entry is the Settings row in the sidebar Foot; clicking it directly opens a 1080×700 centered overlay (black 24% mask); the close button, a mask click, and ESC all close it. There is no intermediate menu form of any kind.
 
 `@deepseek-ai/dsh-client-locale` provides `ctx.locale`; `ui-theme` provides `ctx.theme`. Both services read through a getter, write through a setter, and publish immutable snapshots via typed Cordis change events; each service persists its own preference (storing only the id, with bad values falling back to the default).
 
-General's apply layer subscribes to `locale/change` and `theme/change` and projects the snapshots into the Zustand store declared by that section. React components only read `useStore` and write through the injected setter callbacks, never reading ctx or the services.
+Each feature row's apply layer subscribes to its own change event (locale to `locale/change`, ui-theme to `theme/change`) and projects the snapshot into the slot store declared when that row registered. React components only read `useStore` and write through the injected setter callbacks, never reading ctx or the services.
 
 The theme preference has three states — `light`, `dark`, `system` — defaulting to `system` (when no persisted preference exists or the value is bad). Resolving system belongs to the theme domain: ThemeService holds the `prefers-color-scheme` matchMedia listener (environment sensing, not DOM presentation) and re-emits the snapshot when the preference is system and the system color scheme changes; the snapshot carries both `preference` and the resolved `active` definition.
 
 The theme service never touches the DOM. `ui-layout` reads the Theme getter initially and then subscribes to `theme/change`; the presenter owned by Layout updates `body[data-ds-dark-theme]` and the theme tokens according to `active`. The presenter has no notion of system — it consumes only resolved results.
 
-### First-phase section scope
+### First-phase registration surfaces
 
-| section | Plugin | First-phase content |
+| Registration surface | Owning plugin | First-phase content |
 |---|---|---|
-| General | `ui-settings-general` | Language (Selector dropdown) and Appearance (Light/Dark/System three cubes) genuinely switch; Permission and Tool Call are visual skeletons only, with no write operations |
-| Models | `ui-settings-models` | Navigation item only; the content area is empty |
-| Plugin | no package | Not built this phase, and the navigation does not show the item (an external-link entry with no target never renders; once a later plugin registers the section it appears automatically) |
+| General section (order 0) | built into the `ui-settings` shell | Permission and Tool Call visual skeletons (no write operations) plus the `settings.general.item` slot declaration |
+| Language row (item order 0) | `locale` | Selector dropdown; 中文/English genuinely switch |
+| Appearance row (item order 10) | `ui-theme` | Light/Dark/System three cubes genuinely switch (the selected state reflects preference) |
+| Models section (order 10) | `ui-models` | Navigation item only, with an empty content area; later model-management features land in that package |
+| Plugin | none | Not built this phase, and the navigation does not show the item (once a later plugin feature package registers the section it appears automatically) |
 
-The first phase localizes only the copy inside the Settings overlay (the General rows plus the navigation); copy on other pages is untouched.
+The first phase localizes only the copy inside the Settings overlay; dictionaries stay close to their owners — shell copy (the chrome plus the General skeletons) lives in the `settings` namespace, and feature-row copy lives in each feature package (`settings.locale`, `settings.theme`, `settings.models`).
 
 ### Slot topology
 
 ```text
 root
 └─ sidebar
-   └─ sidebar.settings                 single/root
-      └─ ui-settings
-         └─ settings.section           list/root
-            ├─ general                 ui-settings-general
-            └─ models                  ui-settings-models
+   └─ sidebar.settings                   single/root
+      └─ ui-settings(壳)
+         └─ settings.section             list/root
+            ├─ general (order 0)         ui-settings 壳自带
+            │  └─ settings.general.item  list/root
+            │     ├─ language (0)        locale 注册
+            │     └─ appearance (10)     ui-theme 注册
+            └─ models (order 10)         ui-models 注册
 ```
 
-Section contributions use declaration-aware deferral and do not depend on the client manifest's apply order.
+Section and item contributions both use declaration-aware deferral and do not depend on the client manifest's apply order. The `settings.general.item` SlotMap entry's canonical home is the ui-settings contract; locale/ui-theme, because of the reference cycle (the shell consumes ctx.locale), consume that entry as verbatim duplicated merges, with declaration merging guaranteeing the copies agree.
 
 ### Service contracts
 
@@ -95,13 +102,16 @@ Locale ships with 中文 and English built in; `setLocale`/`setTheme` are the on
 
 **Settings importing and enumerating the sections.** Adding a page would require modifying the shell plugin, breaking the composition model where each feature occupies a slot from its own plugin.
 
+**One `ui-settings-*` package per section (the first-cut implementation).** It divorces the settings surface from the feature itself: changing Theme behavior touches two packages, the package count grows linearly with settings items, and settings-general depending back on the locale/theme services forms an intermediate layer that exists purely for the package split. After converging on feature-owner self-registration, General belongs to the shell (it belongs to no single feature) and preference rows ship with their feature packages.
+
 **Injecting the Locale/Theme snapshots into React directly.** Inject results are cached by entry identity, so volatile values go stale; hand-rolling a React hook per service also bypasses the slot store's unified binding.
 
 ## Acceptance criteria
 
-- The Settings shell depends only on the slot ledger, never on any section implementation.
+- The Settings shell depends only on the slot ledger, never on any feature implementation; General's item list likewise depends only on the ledger.
+- Adding a settings item = the feature package registering it itself (a section or a general item), with zero shell changes.
 - Locale and Theme writes go only through the setters; ongoing synchronization goes only through the change events.
-- The General store initializes from the getters and is thereafter updated by the two events with local re-renders.
+- Each feature row's store initializes from the getter and is thereafter updated by its own change event with local re-renders.
 - Layout applies the theme snapshot on its own and the theme service never accesses the DOM; no system branch appears in the presenter.
 - 中文/English and Light/Dark/System switch and are restored after a refresh; with the preference on system, a system color-scheme change takes effect immediately.
 - Models has only a navigation item and an empty content area; the Permission and Tool Call skeletons perform no writes.

+ 27 - 17
.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.zh.md

@@ -10,41 +10,48 @@ Status: proposed
 
 ## Proposal
 
-Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明 `settings.section` list 坑位。每个 section 由独立插件贡献;Settings 壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。
+**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳只提供组合面(一级 section 列表 + General 内的 item 列表),不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应坑位注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。壳自带的唯一内容是第一个一级目录 General(骨架行 + item 坑位声明),因为它不属于任何单一功能。
+
+Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明 `settings.section` list 坑位。每个 section 由功能插件贡献;Settings 壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由壳自己注册(order 0)并声明 `settings.general.item` list 坑位,功能插件的偏好行按 order 排入。
 
 Settings 入口是 sidebar Foot 的 Settings 行,点击直接打开 1080×700 居中浮层(黑 24% 遮罩);close 按钮、点击遮罩、ESC 均关闭。无任何中间菜单形态。
 
 `@deepseek-ai/dsh-client-locale` 提供 `ctx.locale`,`ui-theme` 提供 `ctx.theme`。两个 service 都以 getter 读取、setter 写入并用 typed Cordis change event 发布 immutable snapshot;service 自己持久化偏好(只存 id,坏值回退默认)。
 
-General 的 apply 层订阅 `locale/change` 和 `theme/change`,把 snapshot 投影到该 section 声明的 Zustand store。React 组件只读 `useStore`、写注入的 setter callback,不读取 ctx 或 service。
+功能行的 apply 层各自订阅自家 change event(locale 订 `locale/change`,ui-theme 订 `theme/change`),把 snapshot 投影到该行注册时声明的 slot store。React 组件只读 `useStore`、写注入的 setter callback,不读取 ctx 或 service。
 
 Theme 偏好三态:`light`、`dark`、`system`,默认 `system`(无持久化偏好或坏值时)。system 的解析属主题领域:ThemeService 持有 `prefers-color-scheme` matchMedia 监听(环境感知,非 DOM 呈现),偏好为 system 且系统配色变化时重发 snapshot;snapshot 同时携带 `preference` 与解析后的 `active` 定义。
 
 Theme service 不操作 DOM。`ui-layout` 初始读取 Theme getter,随后订阅 `theme/change`,由 Layout 持有的 presenter 按 `active` 更新 `body[data-ds-dark-theme]` 和主题 token;presenter 不感知 system,只消费已解析结果。
 
-### 首期 section 范围
+### 首期注册面
 
-| section | 插件 | 首期内容 |
+| 注册面 | 属主插件 | 首期内容 |
 |---|---|---|
-| General | `ui-settings-general` | Language(Selector 下拉)与 Appearance(Light/Dark/System 三 cube)真实可切;Permission、Tool Call 仅视觉骨架,无写操作 |
-| Models | `ui-settings-models` | 仅导航项,内容区为空 |
-| Plugin | 不建包 | 首期不做,导航不出现该项(无目标的外链入口不上屏;后续插件注册 section 即自动出现) |
+| General section(order 0)| `ui-settings` 壳自带 | Permission、Tool Call 视觉骨架(无写操作)+ `settings.general.item` 坑位声明 |
+| Language 行(item order 0)| `locale` | Selector 下拉,中文/English 真实可切 |
+| Appearance 行(item order 10)| `ui-theme` | Light/Dark/System 三 cube 真实可切(选中态看 preference) |
+| Models section(order 10)| `ui-models` | 仅导航项,内容区为空;后续模型管理功能落在该包 |
+| Plugin | 无 | 首期不做,导航不出现该项(后续插件功能包注册 section 即自动出现) |
 
-首期只翻译 Settings 浮层内文案(General 各行 + 导航);其他页面文案不动。
+首期只翻译 Settings 浮层内文案;字典就近——壳文案(chrome + General 骨架)归 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
 
 ### Slot topology
 
 ```text
 root
 └─ sidebar
-   └─ sidebar.settings                 single/root
-      └─ ui-settings
-         └─ settings.section           list/root
-            ├─ general                 ui-settings-general
-            └─ models                  ui-settings-models
+   └─ sidebar.settings                   single/root
+      └─ ui-settings(壳)
+         └─ settings.section             list/root
+            ├─ general (order 0)         ui-settings 壳自带
+            │  └─ settings.general.item  list/root
+            │     ├─ language (0)        locale 注册
+            │     └─ appearance (10)     ui-theme 注册
+            └─ models (order 10)         ui-models 注册
 ```
 
-section contribution 使用 declaration-aware deferral,不依赖 client manifest 的 apply 顺序。
+section/item contribution 均使用 declaration-aware deferral,不依赖 client manifest 的 apply 顺序。`settings.general.item` 的 SlotMap 条目正家在 ui-settings contract;locale/ui-theme 因引用环(壳消费 ctx.locale)以逐字重复合并的方式消费该条目,declaration merging 保证副本一致。
 
 ### Service contracts
 
@@ -95,13 +102,16 @@ Locale 内置中文和 English;`setLocale`/`setTheme` 是唯一写入口,未
 
 **Settings import 并枚举各 section。** 新增页面必须修改壳插件,破坏「每个功能由自己的插件占坑」的组合模型。
 
+**每个 section 单开 `ui-settings-*` 包(首版实现)。** 设置面与功能本体分家:改 Theme 行为要动两个包,包数随设置项线性膨胀,且 settings-general 反向依赖 locale/theme 服务形成纯粹为拆包而生的中间层。收敛为功能属主自注册后,General 归壳(不属任何单一功能),preference 行随功能包交付。
+
 **把 Locale/Theme snapshot 直接注入 React。** inject 结果按 entry identity 缓存,易变值会陈旧;为每个 service 自造 React hook 也绕开 slot store 的统一绑定。
 
 ## Acceptance criteria
 
-- Settings 壳只依赖 slot ledger,不依赖任一 section 实现。
+- Settings 壳只依赖 slot ledger,不依赖任一功能实现;General 的 item 列表同样只依赖 ledger。
+- 新增一个设置项 = 功能包自己注册(section 或 general item),零壳改动。
 - Locale 与 Theme 的写入只走 setter,持续同步只走 change event。
-- General store 初始化走 getter,后续由两个 event 更新并局部重渲染。
+- 功能行 store 初始化走 getter,后续由自家 change event 更新并局部重渲染。
 - Layout 独立应用 Theme snapshot,Theme service 不访问 DOM;presenter 不出现 system 分支。
 - 中文/English 与 Light/Dark/System 能切换并刷新后恢复;偏好为 system 时系统配色变化即时生效。
 - Models 只有导航项与空内容区;Permission、Tool Call 骨架无写操作。
@@ -109,4 +119,4 @@ Locale 内置中文和 English;`setLocale`/`setTheme` 是唯一写入口,未
 
 ## Risks
 
-slot 声明与 contribution 的 apply 顺序不固定,所有新 section 必须保留 declaration-aware registration 和幂等防护。service event 可能早于 section 首次渲染,General store 的 init 与 controller attach 都必须从 getter 对齐当前 snapshot。Layout 卸载时必须清理自己设置的全局属性,ThemeService dispose 时必须移除 matchMedia 监听,避免 HMR 后残留。
+slot 声明与 contribution 的 apply 顺序不固定,所有 section/item 注册方必须保留 declaration-aware registration,并以 ledger(而非本地 disposer)判定在位。service event 可能早于行首次渲染,功能行 store 的 init 与 inject attach 都必须从 getter 对齐当前 snapshot。`settings.general.item` 的重复合并副本(locale、ui-theme)与 ui-settings 正家必须逐字一致,漂移即三处一起改。Layout 卸载时必须清理自己设置的全局属性,ThemeService dispose 时必须移除 matchMedia 监听,避免 HMR 后残留。

+ 2 - 5
apps/cli/cordis.yml

@@ -233,11 +233,8 @@
 - id: ui-settings
   name: '@deepseek-ai/dsh-client-ui-settings'
 
-- id: ui-settings-general
-  name: '@deepseek-ai/dsh-client-ui-settings-general'
-
-- id: ui-settings-models
-  name: '@deepseek-ai/dsh-client-ui-settings-models'
+- id: ui-models
+  name: '@deepseek-ai/dsh-client-ui-models'
 
 - id: ui-conversation
   name: '@deepseek-ai/dsh-client-ui-conversation'

+ 1 - 2
apps/cli/package.json

@@ -31,8 +31,7 @@
     "@deepseek-ai/dsh-client-ui-question": "workspace:^",
     "@deepseek-ai/dsh-client-ui-sidebar": "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-models": "workspace:^",
     "@deepseek-ai/dsh-client-ui-theme": "workspace:^",
     "@deepseek-ai/dsh-client-ui-trajectory": "workspace:^",
     "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",

+ 1 - 4
apps/cli/tsconfig.json

@@ -45,10 +45,7 @@
       "path": "../../packages/client/ui-settings"
     },
     {
-      "path": "../../packages/client/ui-settings-general"
-    },
-    {
-      "path": "../../packages/client/ui-settings-models"
+      "path": "../../packages/client/ui-models"
     },
     {
       "path": "../../packages/client/locale"

+ 1 - 2
apps/web/tests/session-title.snapshot.ts

@@ -14,8 +14,7 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
   { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
   { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
   { id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-locale'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-models', dir: 'ui-settings-models', url: '/plugins/ui-settings-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
+  { id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
   { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
   { id: '@deepseek-ai/dsh-client-ui-workspace', dir: 'ui-workspace', url: '/plugins/ui-workspace.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-sidebar'] },
   { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },

+ 1 - 2
apps/web/tests/workspace-flow.snapshot.ts

@@ -14,8 +14,7 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
   { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
   { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
   { id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-locale'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-models', dir: 'ui-settings-models', url: '/plugins/ui-settings-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
+  { id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
   { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
   {
     id: '@deepseek-ai/dsh-client-ui-workspace',

+ 18 - 4
packages/client/locale/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-client-locale",
-  "description": "Locale plugin: LocaleService (zh/en preference with getter/setter/change event + persistence; ns x locale dictionaries, bind(ns) -> t)",
+  "description": "Locale plugin: LocaleService (zh/en preference with getter/setter/change event + persistence; ns x locale dictionaries, bind(ns) -> t); registers the Language settings row",
   "version": "0.0.1",
   "private": true,
   "type": "module",
@@ -23,18 +23,29 @@
     "./package.json": "./package.json"
   },
   "dshClient": {
-    "inject": [],
+    "inject": [
+      "@deepseek-ai/dsh-client-runtime"
+    ],
     "platform": "web",
     "immediately": true
   },
   "license": "BSD-3-Clause",
   "peerDependencies": {
+    "@deepseek-ai/dsh-client-runtime": "^0.0.1",
+    "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
+    "@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
     "@deepseek-ai/dsh-invariants": "^0.0.1",
-    "cordis": "^4.0.0-rc.7"
+    "cordis": "^4.0.0-rc.7",
+    "react": "^18.2.0"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "cordis": "^4.0.0-rc.7"
+    "@types/react": "~18.3.1",
+    "cordis": "^4.0.0-rc.7",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",
@@ -47,5 +58,8 @@
   "scripts": {
     "bundle": "tsdown",
     "watch": "tsdown --watch"
+  },
+  "dependencies": {
+    "clsx": "^2.0.0"
   }
 }

+ 47 - 0
packages/client/locale/src/client/LanguageRow.module.css

@@ -0,0 +1,47 @@
+/* Language row (figma 'Setting-Cell': gap 8, pad 16/0, hairline separator;
+ * the section column removes the separator on its last child). */
+
+.row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 16px 0;
+  border-bottom: 1px solid var(--dsw-alias-border-l2);
+}
+
+.rowText {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  padding-right: 48px;
+}
+
+.title {
+  font-size: 14px;
+  font-weight: 400;
+  line-height: 22px;
+  color: var(--dsw-alias-label-primary);
+}
+
+/* Selector pill (figma 'Selector': h36 r18, fill #F5F6F7, pad 0/14, gap 12). */
+.selector {
+  display: inline-flex;
+  align-items: center;
+  gap: 12px;
+  height: 36px;
+  padding: 0 14px;
+  border: none;
+  border-radius: 18px;
+  background: var(--dsw-alias-bg-module-platform);
+  font: inherit;
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--dsw-alias-label-primary);
+  cursor: pointer;
+}
+
+.chevron {
+  flex: none;
+}

+ 68 - 0
packages/client/locale/src/client/LanguageRow.tsx

@@ -0,0 +1,68 @@
+/**
+ * Language preference row registered into the General section item slot
+ * (figma 501:30011 'Setting-Cell'): title + selector pill opening the locale
+ * menu. Registered by this package — the locale feature owns its own
+ * settings surface.
+ */
+import { useState } from 'react'
+import type { PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
+import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
+import type {} from './settings-contract.ts'
+import type { createLanguageRowStore } from './settings-store.ts'
+import css from './LanguageRow.module.css'
+
+/** Injected business face: namespace-bound translate + the preference write. */
+export interface LanguageRowInjected {
+  /** Translate a `settings.locale` dictionary key to the active-locale text. */
+  t: (key: string) => string
+  /** Switch the active locale (a registered locale id). */
+  setLocale: (id: string) => void
+}
+
+/** Full component props: runtime share + store share + injected face. */
+export type LanguageRowComponentProps =
+  PropsRuntime<'settings.general.item'> & PropsStore<ReturnType<typeof createLanguageRowStore>> & LanguageRowInjected
+
+/**
+ * Render the Language row.
+ * @param props - composed slot props.
+ * @returns the row element tree.
+ */
+export function LanguageRow({ t, setLocale, useStore }: LanguageRowComponentProps) {
+  const active = useStore(s => s.active)
+  const options = useStore(s => s.options)
+  const [open, setOpen] = useState(false)
+  const activeLabel = options.find(o => o.id === active)?.label ?? active
+
+  return (
+    <div className={css.row}>
+      <div className={css.rowText}>
+        <div className={css.title}>{t('language.title')}</div>
+      </div>
+      <Menu
+        open={open}
+        onClose={() => { setOpen(false) }}
+        items={options.map(o => ({ id: o.id, label: o.label }))}
+        selectedId={active}
+        onSelect={(id) => {
+          setLocale(id)
+          setOpen(false)
+        }}
+        align="end"
+        portal
+        anchor={(
+          <button
+            type="button"
+            className={css.selector}
+            aria-haspopup="menu"
+            aria-expanded={open}
+            onClick={() => { setOpen(v => !v) }}
+          >
+            {activeLabel}
+            <IconChevronDownOutline14 className={css.chevron} />
+          </button>
+        )}
+      />
+    </div>
+  )
+}

+ 67 - 5
packages/client/locale/src/client/index.ts

@@ -1,10 +1,20 @@
 /**
  * Browser-side locale registry. Bound translation functions retain stable
- * identity for injected consumers.
+ * identity for injected consumers. The plugin also registers the Language
+ * preference row into the settings General section — the locale feature owns
+ * its own settings surface.
  */
 import type { Context } from 'cordis'
+import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
+import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
 import { en } from '../locales/en.ts'
 import { zh } from '../locales/zh.ts'
+import type { LanguageRowInjected } from './LanguageRow.tsx'
+import { LanguageRow } from './LanguageRow.tsx'
+import { createLanguageRowStore } from './settings-store.ts'
+
+export type { LanguageRowComponentProps, LanguageRowInjected } from './LanguageRow.tsx'
+export type { LanguageOptionRow, LanguageRowState } from './settings-store.ts'
 
 /** Translate a key with optional params. */
 export type Translate = (key: string, params?: Record<string, unknown>) => string
@@ -53,6 +63,9 @@ export const FALLBACK_LOCALE: LocaleId = 'zh'
 /** Shared namespace for shell-level texts. */
 export const COMMON_NS = 'common'
 
+/** Namespace owning this feature's settings-row copy. */
+export const SETTINGS_NS = 'settings.locale'
+
 /** localStorage key holding the persisted locale id. */
 export const STORAGE_KEY = 'dsh.locale'
 
@@ -183,16 +196,65 @@ function persistPreference(id: LocaleId): void {
   }
 }
 
-/** Required services (none; the loader passes the export surface as an object plugin). */
-export const inject: string[] = []
+/** Required services: the slot registry (the feature registers its own settings row). */
+export const inject = ['slots']
 
 /**
- * Client plugin body: provide the locale service with base dictionaries.
+ * Client plugin body: provide the locale service with base dictionaries and
+ * register the feature-owned Language preference row into the General
+ * section's item slot (a feature owns its settings surface).
  * @param ctx - client cordis context.
  */
-export function apply(ctx: Context): void {
+export function apply(ctx: ClientContext): void {
   const locale = new LocaleService(ctx)
   locale.register(COMMON_NS, 'zh', zh)
   locale.register(COMMON_NS, 'en', en)
+  locale.register(SETTINGS_NS, 'zh', { 'language.title': '语言' })
+  locale.register(SETTINGS_NS, 'en', { 'language.title': 'Language' })
   ctx.provide('locale', locale)
+
+  const store = createLanguageRowStore()
+  let bound: BoundActions<typeof store> | undefined
+  const sync = (snapshot: LocaleSnapshot): void => {
+    bound?.sync(
+      snapshot.active,
+      snapshot.locales.map(l => ({ id: l.id, label: l.label })),
+      snapshot.revision,
+    )
+  }
+  ctx.on('locale/change', sync)
+  const injected = (actions: BoundActions<typeof store>): LanguageRowInjected => {
+    bound = actions
+    // Re-sync from the getter so no event is lost between registration and
+    // first render (the store's revision guard drops stale duplicates).
+    sync(locale.getLocale())
+    return {
+      t: locale.bind(SETTINGS_NS),
+      setLocale: (id) => { locale.setLocale(id) },
+    }
+  }
+  // Declaration-aware registration; the LEDGER is the has-registered judge
+  // (not a local flag): after an HMR collapse re-declares the slot, the
+  // cascade already removed our entry, and a stale disposer must not block
+  // the re-registration.
+  ctx.effect(() => {
+    let dispose: (() => void) | undefined
+    const tryRegister = (): void => {
+      if (ctx.slots.spec('settings.general.item') === undefined) return
+      if (ctx.slots.entries('settings.general.item').some(e => e.component === LanguageRow)) return
+      dispose = ctx.slots.register({
+        name: 'settings.general.item',
+        id: 'language',
+        order: 0,
+        store,
+        inject: injected,
+      }, LanguageRow)
+    }
+    const unsubscribe = ctx.slots.subscribe('settings.general.item', () => { tryRegister() })
+    tryRegister()
+    return () => {
+      unsubscribe()
+      dispose?.()
+    }
+  }, 'locale: language settings row registration')
 }

+ 18 - 0
packages/client/locale/src/client/settings-contract.ts

@@ -0,0 +1,18 @@
+/**
+ * Settings-surface slot merge consumed by this package's Language row. The
+ * AUTHORITATIVE home for 'settings.general.item' is the ui-settings contract
+ * (declaring is claiming: the shell's General entry declares the slot); this
+ * file repeats the entry verbatim because the shell consumes ctx.locale
+ * (project reference ui-settings -> locale), so importing the shell's types
+ * from here would close a reference cycle. TypeScript declaration merging
+ * rejects diverging duplicates, so every program that sees both copies (the
+ * shell's own build, the client aggregate) enforces identity.
+ */
+declare module '@deepseek-ai/dsh-client-ui-slots' {
+  interface SlotMap {
+    /** One preference row inside the General section (duplicate-identical merge; authority: ui-settings contract). */
+    'settings.general.item': { kind: 'list'; scope: 'root'; owner: { children?: never } }
+  }
+}
+
+export {}

+ 47 - 0
packages/client/locale/src/client/settings-store.ts

@@ -0,0 +1,47 @@
+/**
+ * Language row slot store: a mirror of the locale service snapshot. The
+ * plugin's apply-world change listener is the only writer; the row component
+ * reads via props.useStore.
+ */
+import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client'
+
+/** One selectable locale row (id + self-described label). */
+export interface LanguageOptionRow {
+  /** Locale id (the setLocale argument). */
+  id: string
+  /** Display name in its own language (中文 / English). */
+  label: string
+}
+
+/** Store state mirrored from the locale snapshot. */
+export interface LanguageRowState {
+  /** Active locale id. */
+  active: string
+  /** Selectable locales in display order. */
+  options: LanguageOptionRow[]
+  /** Service revision; -1 until first sync so revision 0 lands as a change. */
+  revision: number
+}
+
+/** Declared action shape giving the exported factory a stable return type. */
+type LanguageRowActions = {
+  sync: (draft: LanguageRowState, active: string, options: LanguageOptionRow[], revision: number) => void
+}
+
+/**
+ * Declares the Language row state and write surface.
+ * @returns the store handle.
+ */
+export function createLanguageRowStore(): EngineStoreHandle<LanguageRowState, LanguageRowActions> {
+  return defineStore({
+    init: (): LanguageRowState => ({ active: '', options: [], revision: -1 }),
+    actions: {
+      sync: (d, active: string, options: LanguageOptionRow[], revision: number) => {
+        if (revision <= d.revision) return
+        d.active = active
+        d.options = options
+        d.revision = revision
+      },
+    },
+  })
+}

+ 0 - 0
packages/client/ui-settings-general/src/css-modules.d.ts → packages/client/locale/src/css-modules.d.ts


+ 5 - 1
packages/client/locale/tests/invariant.spec.ts

@@ -1,8 +1,10 @@
+// @vitest-environment jsdom
 import { describe, expect, it } from 'vitest'
 import { Context } from 'cordis'
 import { apply as nodeApply } from '@deepseek-ai/dsh-client-locale'
 import { apply as clientApply, COMMON_NS, LocaleService, inject } from '@deepseek-ai/dsh-client-locale/client'
 import * as LocaleInvariant from '@deepseek-ai/dsh-client-locale/invariant'
+import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
 import InvariantService from '@deepseek-ai/dsh-invariants'
 
 describe('invariant companion', () => {
@@ -18,8 +20,10 @@ describe('invariant companion', () => {
   })
 
   it('client apply provides ctx.locale seeded with the zh/en common namespace', async () => {
-    expect(inject).toEqual([])
+    // The feature registers its own Language settings row, hence the slots edge.
+    expect(inject).toEqual(['slots'])
     const ctx = new Context()
+    new SlotsService(ctx)
     await ctx.plugin({ inject, apply: clientApply }).await()
     const locale = ctx.get('locale')
     expect(locale).toBeInstanceOf(LocaleService)

+ 9 - 0
packages/client/locale/tsconfig.json

@@ -8,6 +8,15 @@
     "src"
   ],
   "references": [
+    {
+      "path": "../runtime"
+    },
+    {
+      "path": "../ui-primitives"
+    },
+    {
+      "path": "../ui-slots"
+    },
     {
       "path": "../../../vendor/cordis"
     },

+ 1 - 1
packages/client/ui-settings-models/README.md → packages/client/ui-models/README.md

@@ -1,4 +1,4 @@
-# @deepseek-ai/dsh-client-ui-settings-models
+# @deepseek-ai/dsh-client-ui-models
 
 Models settings section plugin: registers the `models` nav entry into `settings.section` with an intentionally empty content column — model management lands in a later phase.
 

+ 2 - 2
packages/client/ui-settings-models/package.json → packages/client/ui-models/package.json

@@ -1,6 +1,6 @@
 {
-  "name": "@deepseek-ai/dsh-client-ui-settings-models",
-  "description": "Models settings section plugin: nav entry with an empty content column (model management lands later)",
+  "name": "@deepseek-ai/dsh-client-ui-models",
+  "description": "Models feature plugin: registers its Settings section (nav entry, empty content column; model management lands later)",
   "version": "0.0.1",
   "private": true,
   "type": "module",

+ 0 - 0
packages/client/ui-settings-models/src/client/ModelsSection.tsx → packages/client/ui-models/src/client/ModelsSection.tsx


+ 2 - 2
packages/client/ui-settings-models/src/client/index.ts → packages/client/ui-models/src/client/index.ts

@@ -30,7 +30,7 @@ export function apply(ctx: ClientContext): void {
       ctx.locale.register('settings.models', 'en', { nav: 'Models' }),
     ]
     return () => { for (const dispose of disposers) dispose() }
-  }, 'ui-settings-models: nav copy dictionaries')
+  }, 'ui-models: nav copy dictionaries')
   // Declaration-aware registration; the LEDGER is the has-registered judge
   // (not a local flag): after an HMR collapse re-declares the slot, the
   // cascade already removed our entry, and a stale disposer must not block
@@ -63,5 +63,5 @@ export function apply(ctx: ClientContext): void {
       unsubscribe()
       dispose?.()
     }
-  }, 'ui-settings-models: section registration')
+  }, 'ui-models: settings section registration')
 }

+ 0 - 0
packages/client/ui-settings-models/src/css-modules.d.ts → packages/client/ui-models/src/css-modules.d.ts


+ 0 - 0
packages/client/ui-settings-models/src/index.ts → packages/client/ui-models/src/index.ts


+ 4 - 4
packages/client/ui-settings-models/src/invariant.ts → packages/client/ui-models/src/invariant.ts

@@ -1,16 +1,16 @@
 /**
- * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-settings-models`.
- * @module @deepseek-ai/dsh-client-ui-settings-models/invariant
+ * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-models`.
+ * @module @deepseek-ai/dsh-client-ui-models/invariant
  */
 
 /* jscpd:ignore-start */
 import type { Context } from 'cordis'
 import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
 
-const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-settings-models'
+const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-models'
 
 /** Cordis companion plugin name. */
-export const name = 'client-ui-settings-models-invariant'
+export const name = 'client-ui-models-invariant'
 /** Service required before the companion can reserve package ownership. */
 export const inject = ['invariants']
 

+ 2 - 2
packages/client/ui-settings-models/tests/apply.spec.ts → packages/client/ui-models/tests/apply.spec.ts

@@ -3,7 +3,7 @@ import { Context } from 'cordis'
 import { describe, expect, it } from 'vitest'
 import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
 import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
-import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-models/client'
+import { apply, inject } from '@deepseek-ai/dsh-client-ui-models/client'
 import { ModelsSection } from '../src/client/ModelsSection.tsx'
 
 async function bench() {
@@ -21,7 +21,7 @@ function declare(slots: SlotsService): () => void {
   )
 }
 
-describe('ui-settings-models apply', () => {
+describe('ui-models apply', () => {
   it('declares the services it uses', () => {
     expect(inject).toEqual(['slots', 'locale'])
   })

+ 2 - 2
packages/client/ui-settings-models/tests/invariant.spec.ts → packages/client/ui-models/tests/invariant.spec.ts

@@ -1,6 +1,6 @@
 import { describe, expect, it } from 'vitest'
 import { Context } from 'cordis'
-import * as ModelsInvariant from '@deepseek-ai/dsh-client-ui-settings-models/invariant'
+import * as ModelsInvariant from '@deepseek-ai/dsh-client-ui-models/invariant'
 import InvariantService from '@deepseek-ai/dsh-invariants'
 import { ModelsSection } from '../src/client/ModelsSection.tsx'
 
@@ -12,7 +12,7 @@ describe('invariant companion', () => {
   })
 
   it('node-half apply is a no-op host placeholder', async () => {
-    const { apply } = await import('@deepseek-ai/dsh-client-ui-settings-models')
+    const { apply } = await import('@deepseek-ai/dsh-client-ui-models')
     apply()
     expect(true).toBe(true) // reaching here without throw is the contract
   })

+ 0 - 0
packages/client/ui-settings-models/tsconfig.json → packages/client/ui-models/tsconfig.json


+ 3 - 0
packages/client/ui-models/tsdown.config.ts

@@ -0,0 +1,3 @@
+import { clientBundle } from '../tsdown.client.ts'
+
+export default clientBundle('@deepseek-ai/dsh-client-ui-models', ['lib/types/index.js', 'lib/types/invariant.js'])

+ 0 - 15
packages/client/ui-settings-general/README.md

@@ -1,15 +0,0 @@
-# @deepseek-ai/dsh-client-ui-settings-general
-
-General settings section plugin: registers the `general` entry into `settings.section`. Language (中文/English) and Appearance (Light/Dark/System) are live preferences wired to `ctx.locale` / `ctx.theme`; Permission and Tool Call rows are visual skeletons with no write surface.
-
-## Model Experience
-
-None, as the section renders browser preference UI; nothing here reaches a model request.
-
-#### KV Cache effect
-
-None; this package neither assembles nor sends a provider request.
-
-## Known Limitations and Deferred Work
-
-- **Permission and Tool Call are display skeletons** — the backing host services and RPC methods do not exist yet; the controls are disabled and write nothing.

+ 0 - 70
packages/client/ui-settings-general/package.json

@@ -1,70 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-client-ui-settings-general",
-  "description": "General settings section plugin: Language and Appearance preferences (live), Permission and Tool Call skeleton rows",
-  "version": "0.0.1",
-  "private": true,
-  "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"
-  },
-  "dshClient": {
-    "inject": [
-      "@deepseek-ai/dsh-client-runtime",
-      "@deepseek-ai/dsh-client-ui-settings",
-      "@deepseek-ai/dsh-client-locale",
-      "@deepseek-ai/dsh-client-ui-theme"
-    ],
-    "platform": "web"
-  },
-  "scripts": {
-    "bundle": "tsdown",
-    "watch": "tsdown --watch"
-  },
-  "license": "BSD-3-Clause",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
-  "peerDependencies": {
-    "@deepseek-ai/dsh-client-runtime": "^0.0.1",
-    "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
-    "@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
-    "@deepseek-ai/dsh-invariants": "^0.0.1",
-    "cordis": "^4.0.0-rc.7",
-    "react": "^18.2.0"
-  },
-  "devDependencies": {
-    "@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-client-ui-theme": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@types/react": "~18.3.1",
-    "cordis": "^4.0.0-rc.7",
-    "react": "^18.2.0"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/invariant.js",
-    "lib/client.js",
-    "lib/types/**/*.d.ts",
-    "lib/types/**/*.d.ts.map",
-    "src"
-  ]
-}

+ 0 - 118
packages/client/ui-settings-general/src/client/GeneralSection.tsx

@@ -1,118 +0,0 @@
-/**
- * General settings section: Permission and Tool Call skeleton rows (visual
- * only, no interaction), live Language and Appearance preference rows wired
- * through the injected setLocale/setTheme callbacks and the snapshot-mirror
- * store. Figma: Settings > Content > Options (501:29983).
- */
-import { useState } from 'react'
-import clsx from 'clsx'
-import {
-  IconChevronDownOutline14, IconDarkOutline16, IconFollowsystemOutline16, IconLightOutline16,
-  Menu,
-} from '@deepseek-ai/dsh-client-ui-primitives'
-import type { GeneralSectionComponentProps, ThemePreferenceId } from './contract.ts'
-import css from './GeneralSection.module.css'
-
-/** Appearance cube order and icons (figma 501:30015-30017: Light, Dark, System). */
-const THEME_CUBES: readonly { id: ThemePreferenceId; labelKey: string; Icon: typeof IconLightOutline16 }[] = [
-  { id: 'light', labelKey: 'appearance.light', Icon: IconLightOutline16 },
-  { id: 'dark', labelKey: 'appearance.dark', Icon: IconDarkOutline16 },
-  { id: 'system', labelKey: 'appearance.system', Icon: IconFollowsystemOutline16 },
-]
-
-/**
- * Render the General section content column.
- * @param props - composed slot props (contract.ts).
- * @returns the section element tree.
- */
-export function GeneralSection(props: GeneralSectionComponentProps) {
-  const { t, setLocale, setTheme, useStore } = props
-  const localeActive = useStore(s => s.localeActive)
-  const localeOptions = useStore(s => s.localeOptions)
-  const themePreference = useStore(s => s.themePreference)
-  const [languageOpen, setLanguageOpen] = useState(false)
-
-  const activeLocaleLabel = localeOptions.find(l => l.id === localeActive)?.label ?? localeActive
-
-  return (
-    <div className={css.section}>
-      {/* Permission (skeleton): disabled selector pill. */}
-      <div className={css.row}>
-        <div className={css.rowText}>
-          <div className={css.title}>{t('permission.title')}</div>
-          <div className={css.desc}>{t('permission.desc')}</div>
-        </div>
-        <button type="button" className={css.selector} disabled>
-          {t('permission.value')}
-          <IconChevronDownOutline14 className={css.chevron} />
-        </button>
-      </div>
-
-      {/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */}
-      <div className={css.group}>
-        <div className={css.title}>{t('toolcall.title')}</div>
-        <div className={css.cubeRow}>
-          <div className={clsx(css.modeCube, css.selected)}>
-            <div className={css.title}>{t('toolcall.schema.title')}</div>
-            <div className={css.desc}>{t('toolcall.schema.desc')}</div>
-          </div>
-          <div className={css.modeCube}>
-            <div className={css.title}>{t('toolcall.code.title')}</div>
-            <div className={css.desc}>{t('toolcall.code.desc')}</div>
-          </div>
-        </div>
-      </div>
-
-      {/* Language: selector pill opens the locale menu. */}
-      <div className={css.row}>
-        <div className={css.rowText}>
-          <div className={css.title}>{t('language.title')}</div>
-        </div>
-        <Menu
-          open={languageOpen}
-          onClose={() => { setLanguageOpen(false) }}
-          items={localeOptions.map(l => ({ id: l.id, label: l.label }))}
-          selectedId={localeActive}
-          onSelect={(id) => {
-            setLocale(id)
-            setLanguageOpen(false)
-          }}
-          align="end"
-          portal
-          anchor={(
-            <button
-              type="button"
-              className={css.selector}
-              aria-haspopup="menu"
-              aria-expanded={languageOpen}
-              onClick={() => { setLanguageOpen(v => !v) }}
-            >
-              {activeLocaleLabel}
-              <IconChevronDownOutline14 className={css.chevron} />
-            </button>
-          )}
-        />
-      </div>
-
-      {/* Appearance: three preference cubes; selection follows the persisted
-        * preference, never the resolved active theme. */}
-      <div className={clsx(css.group, css.last)}>
-        <div className={css.title}>{t('appearance.title')}</div>
-        <div className={css.cubeRow}>
-          {THEME_CUBES.map(({ id, labelKey, Icon }) => (
-            <button
-              key={id}
-              type="button"
-              className={clsx(css.themeCube, themePreference === id && css.selected)}
-              aria-pressed={themePreference === id}
-              onClick={() => { setTheme(id) }}
-            >
-              <Icon />
-              {t(labelKey)}
-            </button>
-          ))}
-        </div>
-      </div>
-    </div>
-  )
-}

+ 0 - 66
packages/client/ui-settings-general/src/client/contract.ts

@@ -1,66 +0,0 @@
-/**
- * General section component contract: the slot-store state shape, the
- * injected business face, and the composed props type. The component imports
- * only from here; service snapshot shapes are mirrored as plain rows so the
- * presentation layer stays decoupled from the locale/theme packages.
- */
-import type { PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
-// Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
-import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
-import type { createGeneralSettingsStore } from './store.ts'
-
-/** One selectable locale row projected into the store (id + self-described label). */
-export interface LocaleOptionRow {
-  /** Locale id (the setLocale argument). */
-  id: string
-  /** Display name in its own language (中文 / English). */
-  label: string
-}
-
-/** Theme preference union mirrored from the theme service snapshot. */
-export type ThemePreferenceId = 'light' | 'dark' | 'system'
-
-/**
- * Store state: mirrors of the locale/theme service snapshots, written only by
- * the plugin's apply-world change listeners (components have no write path —
- * preference writes go through the injected callbacks to the services, and
- * the resulting change events flow back into this mirror).
- */
-export interface GeneralSettingsState {
-  /** Active locale id. */
-  localeActive: string
-  /** Selectable locales in display order. */
-  localeOptions: LocaleOptionRow[]
-  /** Locale service revision (re-renders translated copy on dictionary/locale changes); -1 until first sync. */
-  localeRevision: number
-  /** Persisted theme preference (selection state reads this, never the resolved active theme). */
-  themePreference: ThemePreferenceId
-  /** Theme service revision; -1 until first sync. */
-  themeRevision: number
-}
-
-/**
- * Registrant-private injected share of the General section (assembled in
- * apply): the namespace-bound translate function (stable identity — re-render
- * on locale change comes from the store revision, not from `t`) and the two
- * preference write callbacks.
- */
-export interface GeneralSectionInjected {
-  /** Translate a `settings.general` dictionary key to the active-locale text. */
-  t: (key: string) => string
-  /** Switch the active locale (a registered locale id). */
-  setLocale: (id: string) => void
-  /** Switch the theme preference. */
-  setTheme: (id: ThemePreferenceId) => void
-}
-
-/** Store handle type for the props share (type-only; the factory stays internal to apply and tests). */
-export type GeneralSettingsStoreHandle = ReturnType<typeof createGeneralSettingsStore>
-
-/**
- * Full component props of the General section: the section owner share
- * (empty marker) plus the store share and the injected face. No child slots
- * are declared; menu open state is component-local viewing state.
- */
-export type GeneralSectionComponentProps =
-  PropsRuntime<'settings.section'> & PropsStore<GeneralSettingsStoreHandle> & GeneralSectionInjected

+ 0 - 117
packages/client/ui-settings-general/src/client/index.ts

@@ -1,117 +0,0 @@
-/**
- * General settings section plugin, browser half. Registers the `general`
- * entry into the shell-declared `settings.section` list slot; Language and
- * Appearance are live preferences projected from ctx.locale / ctx.theme
- * through this entry's slot store. Export discipline: packages/client/AGENTS.md.
- */
-import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
-import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
-// Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
-import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
-// Type-only: the locale/theme Context+Events merges and snapshot shapes.
-import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client'
-import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
-import type { GeneralSectionInjected } from './contract.ts'
-import { createGeneralSettingsStore } from './store.ts'
-import { en, zh } from './locales.ts'
-import { GeneralSection } from './GeneralSection.tsx'
-
-export type {
-  GeneralSectionComponentProps, GeneralSectionInjected, GeneralSettingsState,
-  GeneralSettingsStoreHandle, LocaleOptionRow, ThemePreferenceId,
-} from './contract.ts'
-
-/** Dictionary namespace owned by this section (also the nav-label reference prefix). */
-const NS = 'settings.general'
-
-/**
- * Required services (cordis fiber inject). The target slot is declared by
- * ui-settings' apply, whose activation order relative to this one is NOT
- * constrained; registration goes through declaration-aware deferral.
- */
-export const inject = ['slots', 'locale', 'theme']
-
-/**
- * Register the `settings.general` dictionaries and the General section entry
- * once the `settings.section` declaration is on the ledger. The slot store
- * mirrors the locale/theme snapshots: change listeners attach here in apply,
- * write through the bound actions captured at inject time, and the inject
- * factory re-syncs from the getters so no event is lost between registration
- * and first render (the store's revision guard drops stale duplicates).
- * @param ctx - client root context.
- */
-export function apply(ctx: ClientContext): void {
-  ctx.effect(() => {
-    const disposeZh = ctx.locale.register(NS, 'zh', zh)
-    const disposeEn = ctx.locale.register(NS, 'en', en)
-    return () => {
-      disposeZh()
-      disposeEn()
-    }
-  }, 'ui-settings-general: dictionaries')
-
-  const store = createGeneralSettingsStore()
-  let bound: BoundActions<typeof store> | undefined
-
-  const syncLocale = (snapshot: LocaleSnapshot): void => {
-    bound?.syncLocale(
-      snapshot.active,
-      snapshot.locales.map(l => ({ id: l.id, label: l.label })),
-      snapshot.revision,
-    )
-  }
-  const syncTheme = (snapshot: ThemeSnapshot): void => {
-    bound?.syncTheme(snapshot.preference, snapshot.revision)
-  }
-  ctx.on('locale/change', syncLocale)
-  ctx.on('theme/change', syncTheme)
-
-  const injected = (actions: BoundActions<typeof store>): GeneralSectionInjected => {
-    bound = actions
-    syncLocale(ctx.locale.getLocale())
-    syncTheme(ctx.theme.getTheme())
-    return {
-      t: ctx.locale.bind(NS),
-      setLocale: (id) => { ctx.locale.setLocale(id) },
-      setTheme: (id) => { ctx.theme.setTheme(id) },
-    }
-  }
-
-  ctx.effect(() => {
-    let dispose: (() => void) | undefined
-    // Presence is judged on the ledger, not on the local disposer: an HMR
-    // collapse of the declaring entry removes this entry from the slot core
-    // while `dispose` stays set (the stale disposer is a no-op), so a local
-    // guard would block the re-registration when the declaration returns.
-    const registered = (): boolean =>
-      ctx.slots.entries('settings.section').some(e => e.component === GeneralSection)
-    const tryRegister = (): void => {
-      if (ctx.slots.spec('settings.section') === undefined || registered()) return
-      dispose = ctx.slots.register({
-        name: 'settings.section',
-        id: 'general',
-        order: 0,
-        label: ctx.locale.bind(NS)('nav'),
-        store,
-        inject: injected,
-      }, GeneralSection)
-    }
-    // Nav labels are registrant-localized: re-register on locale change so
-    // the ledger carries fresh text (the version bump re-renders the shell).
-    // The ledger check mirrors tryRegister: after an HMR collapse `dispose`
-    // stays set while the entry is gone — relabeling then must stay quiet.
-    const offLocale = ctx.on('locale/change', () => {
-      if (dispose === undefined || !registered()) return
-      dispose()
-      dispose = undefined
-      tryRegister()
-    })
-    const unsubscribe = ctx.slots.subscribe('settings.section', () => { tryRegister() })
-    tryRegister()
-    return () => {
-      offLocale()
-      unsubscribe()
-      dispose?.()
-    }
-  }, 'ui-settings-general: section registration')
-}

+ 0 - 43
packages/client/ui-settings-general/src/client/store.ts

@@ -1,43 +0,0 @@
-/**
- * General section slot store: locale/theme snapshot mirrors. The plugin
- * creates the handle at apply time (identity follows the fiber) and its
- * change listeners are the only writers; components read via props.useStore.
- */
-import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client'
-import type { GeneralSettingsState, LocaleOptionRow, ThemePreferenceId } from './contract.ts'
-
-/** Declared action shape used to give the exported factory a stable return type. */
-type GeneralSettingsActions = {
-  syncLocale: (draft: GeneralSettingsState, active: string, options: LocaleOptionRow[], revision: number) => void
-  syncTheme: (draft: GeneralSettingsState, preference: ThemePreferenceId, revision: number) => void
-}
-
-/**
- * Declares the General section state and write surface. Revisions start at -1
- * so the apply-time initial sync (revision 0) always lands as a change.
- * @returns the store handle.
- */
-export function createGeneralSettingsStore(): EngineStoreHandle<GeneralSettingsState, GeneralSettingsActions> {
-  return defineStore({
-    init: (): GeneralSettingsState => ({
-      localeActive: '',
-      localeOptions: [],
-      localeRevision: -1,
-      themePreference: 'system',
-      themeRevision: -1,
-    }),
-    actions: {
-      syncLocale: (d, active: string, options: LocaleOptionRow[], revision: number) => {
-        if (revision <= d.localeRevision) return
-        d.localeActive = active
-        d.localeOptions = options
-        d.localeRevision = revision
-      },
-      syncTheme: (d, preference: ThemePreferenceId, revision: number) => {
-        if (revision <= d.themeRevision) return
-        d.themePreference = preference
-        d.themeRevision = revision
-      },
-    },
-  })
-}

+ 0 - 4
packages/client/ui-settings-general/src/index.ts

@@ -1,4 +0,0 @@
-/** Host loader entry for the browser implementation exported from `./client`. */
-
-/** Host plugin body — no host-side behavior for the general settings plugin. */
-export function apply(): void {}

+ 0 - 32
packages/client/ui-settings-general/src/invariant.ts

@@ -1,32 +0,0 @@
-/**
- * Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-settings-general`.
- * @module @deepseek-ai/dsh-client-ui-settings-general/invariant
- */
-
-/* jscpd:ignore-start */
-import type { Context } from 'cordis'
-import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
-
-const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-settings-general'
-
-/** Cordis companion plugin name. */
-export const name = 'client-ui-settings-general-invariant'
-/** Service required before the companion can reserve package ownership. */
-export const inject = ['invariants']
-
-/**
- * No runtime invariant: a section plugin projecting two service change events
- * into its own slot store — it emits no cordis events of its own and owns no
- * cross-plugin mutable relation; snapshot/store agreement is asserted by this
- * package's behavior specs.
- */
-const install: InvariantInstaller = () => {}
-
-/**
- * Register this package's invariant companion.
- * @param ctx - Cordis context carrying the invariant service.
- * @returns the installed registration's disposer after setup succeeds.
- */
-export const apply = (ctx: Context): Promise<() => void> =>
-  Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
-/* jscpd:ignore-end */

+ 0 - 139
packages/client/ui-settings-general/tests/apply.spec.ts

@@ -1,139 +0,0 @@
-/** apply wiring: dictionary registration, declaration-aware section entry,
- * snapshot projection into the slot store, locale-driven relabeling, and
- * recovery after an HMR collapse of the declaring entry. */
-import { Context } from 'cordis'
-import { describe, expect, it } from 'vitest'
-import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
-import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
-import { ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client'
-import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
-import type { GeneralSectionInjected } from '@deepseek-ai/dsh-client-ui-settings-general/client'
-import { GeneralSection } from '../src/client/GeneralSection.tsx'
-import type { createGeneralSettingsStore } from '../src/client/store.ts'
-
-const NS = 'settings.general'
-
-async function bench() {
-  const ctx = new Context()
-  await ctx.plugin(SlotsService).await()
-  const locale = new LocaleService(ctx)
-  const theme = new ThemeService(ctx)
-  ctx.provide('locale', locale)
-  ctx.provide('theme', theme)
-  return { ctx, slots: ctx.get('slots') as SlotsService, locale, theme }
-}
-
-/** Stand in for the settings shell: declare the section list slot from root. */
-function declareSection(slots: SlotsService): () => void {
-  return slots.register(
-    { name: 'root', children: { 'settings.section': { kind: 'list', scope: 'root' } } } as never,
-    () => null,
-  )
-}
-
-/** Mirror the framework's inject choreography: bake a real instance from the
- * declared handle and hand its actions to the entry's inject factory. */
-function faceOf(slots: SlotsService) {
-  const entry = slots.entries('settings.section')[0]!
-  const handle = entry.store as ReturnType<typeof createGeneralSettingsStore>
-  const instance = handle.create()
-  const face = (entry.inject as unknown as (a: typeof instance.actions) => GeneralSectionInjected)(instance.actions)
-  return { entry, instance, face }
-}
-
-describe('ui-settings-general apply', () => {
-  it('declares the slot, locale, and theme services', () => {
-    expect(inject).toEqual(['slots', 'locale', 'theme'])
-  })
-
-  it('registers dictionaries and the section entry for declarations before or after apply', async () => {
-    const before = await bench()
-    declareSection(before.slots)
-    await before.ctx.plugin({ inject: [...inject], apply }).await()
-    const entry = before.slots.entries('settings.section')[0]!
-    expect(entry.component).toBe(GeneralSection)
-    expect(entry.options).toMatchObject({ id: 'general', order: 0, label: '通用设置' })
-    expect(before.locale.bind(NS)('nav')).toBe('通用设置')
-
-    const after = await bench()
-    const fiber = after.ctx.plugin({ inject: [...inject], apply })
-    await fiber.await()
-    expect(after.slots.entries('settings.section')).toHaveLength(0)
-    declareSection(after.slots)
-    await Promise.resolve()
-    expect(after.slots.entries('settings.section')[0]!.component).toBe(GeneralSection)
-    // Teardown without a live registration exercises the undefined-disposer arm.
-    await fiber.dispose()
-    expect(after.slots.entries('settings.section')).toHaveLength(0)
-  })
-
-  it('projects service snapshots into the store and routes face writes back', async () => {
-    const b = await bench()
-    declareSection(b.slots)
-    await b.ctx.plugin({ inject: [...inject], apply }).await()
-    // Events ahead of any inject hit the unbound-actions arm without a store.
-    b.theme.setTheme('dark')
-
-    const { instance, face } = faceOf(b.slots)
-    // The inject-time re-sync sealed the init window: both mirrors are current.
-    expect(instance.getSnapshot().localeActive).toBe('zh')
-    expect(instance.getSnapshot().localeOptions.map(l => l.id)).toEqual(['zh', 'en'])
-    expect(instance.getSnapshot().themePreference).toBe('dark')
-    expect(face.t('nav')).toBe('通用设置')
-
-    face.setLocale('en')
-    expect(b.locale.getLocale().active).toBe('en')
-    expect(instance.getSnapshot().localeActive).toBe('en')
-    expect(face.t('nav')).toBe('General')
-
-    face.setTheme('system')
-    expect(b.theme.getTheme().preference).toBe('system')
-    expect(instance.getSnapshot().themePreference).toBe('system')
-  })
-
-  it('re-registers with a fresh ledger label when the locale changes', async () => {
-    const b = await bench()
-    declareSection(b.slots)
-    await b.ctx.plugin({ inject: [...inject], apply }).await()
-    expect(b.slots.entries('settings.section')[0]!.options.label).toBe('通用设置')
-    b.locale.setLocale('en')
-    const entry = b.slots.entries('settings.section')[0]!
-    expect(entry.options.label).toBe('General')
-    expect(entry.component).toBe(GeneralSection)
-  })
-
-  it('recovers after an HMR collapse of the declaring entry (stale disposer must not block)', async () => {
-    const b = await bench()
-    const host = declareSection(b.slots)
-    await b.ctx.plugin({ inject: [...inject], apply }).await()
-    expect(b.slots.entries('settings.section')).toHaveLength(1)
-
-    // Collapse: the declarer dies, the cascade removes our entry while the
-    // apply closure still holds its (now stale) disposer.
-    host()
-    expect(b.slots.entries('settings.section')).toHaveLength(0)
-
-    // A locale change inside the collapsed window must stay quiet.
-    b.locale.setLocale('en')
-    expect(b.slots.entries('settings.section')).toHaveLength(0)
-
-    // Redeclaration restores the entry — with the current locale's label.
-    declareSection(b.slots)
-    await Promise.resolve()
-    const entry = b.slots.entries('settings.section')[0]!
-    expect(entry.component).toBe(GeneralSection)
-    expect(entry.options.label).toBe('General')
-  })
-
-  it('removes the entry and the dictionaries on teardown', async () => {
-    const b = await bench()
-    declareSection(b.slots)
-    const fiber = b.ctx.plugin({ inject: [...inject], apply })
-    await fiber.await()
-    expect(b.slots.entries('settings.section')).toHaveLength(1)
-    await fiber.dispose()
-    expect(b.slots.entries('settings.section')).toHaveLength(0)
-    // Dictionary disposal: translation falls back to the bare key.
-    expect(b.locale.bind(NS)('nav')).toBe('nav')
-  })
-})

+ 0 - 112
packages/client/ui-settings-general/tests/general-section.spec.tsx

@@ -1,112 +0,0 @@
-// @vitest-environment jsdom
-/** GeneralSection behavior: skeleton rows stay inert, Language menu drives
- * setLocale, Appearance cubes follow the preference and drive setTheme. */
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
-import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
-import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
-import { GeneralSection } from '../src/client/GeneralSection.tsx'
-import { createGeneralSettingsStore } from '../src/client/store.ts'
-import { en } from '../src/client/locales.ts'
-import type { GeneralSectionComponentProps } from '../src/client/contract.ts'
-
-afterEach(cleanup)
-
-const LOCALES = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }]
-
-/** Empty global standard-kit hooks (the section reads neither). */
-function emptySessions() {
-  const store = createSnapshotStore<SessionListState>(
-    { ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' })
-  return bindSnapshotSelector(store)
-}
-function emptyWorkspaces() {
-  const store = createSnapshotStore<WorkspaceListState>({
-    items: [], intent: undefined, state: 'idle', phase: 'ready', error: null,
-    baselinesReady: true, recentWorkspaceId: undefined,
-  })
-  return bindSnapshotSelector(store)
-}
-
-function mount(init?: { active?: string; preference?: 'light' | 'dark' | 'system' }) {
-  // Real store instance — the sanctioned zero-machinery path for tests.
-  const store = createGeneralSettingsStore().create()
-  store.actions.syncLocale(init?.active ?? 'en', LOCALES, 0)
-  store.actions.syncTheme(init?.preference ?? 'system', 0)
-  const setLocale = vi.fn()
-  const setTheme = vi.fn()
-  const props: GeneralSectionComponentProps = {
-    useSessions: emptySessions(),
-    useWorkspaces: emptyWorkspaces(),
-    useStore: bindSnapshotSelector(store),
-    actions: store.actions,
-    t: (key: string) => en[key] ?? key,
-    setLocale,
-    setTheme,
-  }
-  render(<GeneralSection {...props} />)
-  return { store, setLocale, setTheme }
-}
-
-const pressed = (name: RegExp): string | null =>
-  screen.getByRole('button', { name }).getAttribute('aria-pressed')
-
-describe('GeneralSection', () => {
-  it('renders the four groups with skeleton rows inert', () => {
-    const b = mount()
-    // Permission: disabled selector showing the fixed value.
-    const permission = screen.getByRole('button', { name: /Read only/ }) as HTMLButtonElement
-    expect(permission.disabled).toBe(true)
-    fireEvent.click(permission)
-    // Tool Call: both mode cubes render as plain text, no buttons.
-    expect(screen.getByText('Schema mode')).toBeDefined()
-    expect(screen.getByText('Code mode')).toBeDefined()
-    expect(screen.queryByRole('button', { name: /Schema mode/ })).toBeNull()
-    expect(b.setLocale).not.toHaveBeenCalled()
-    expect(b.setTheme).not.toHaveBeenCalled()
-  })
-
-  it('opens the language menu, selects a locale, and closes', () => {
-    const b = mount({ active: 'en' })
-    const trigger = screen.getByRole('button', { name: /English/ })
-    expect(trigger.getAttribute('aria-expanded')).toBe('false')
-    fireEvent.click(trigger)
-    expect(trigger.getAttribute('aria-expanded')).toBe('true')
-    fireEvent.click(screen.getByRole('menuitem', { name: '中文' }))
-    expect(b.setLocale).toHaveBeenCalledWith('zh')
-    expect(trigger.getAttribute('aria-expanded')).toBe('false')
-    expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull()
-  })
-
-  it('closes the language menu on outside pointerdown without selecting', () => {
-    const b = mount({ active: 'en' })
-    const trigger = screen.getByRole('button', { name: /English/ })
-    fireEvent.click(trigger)
-    expect(screen.getByRole('menuitem', { name: '中文' })).toBeDefined()
-    fireEvent.pointerDown(document.body)
-    expect(trigger.getAttribute('aria-expanded')).toBe('false')
-    expect(screen.queryByRole('menuitem', { name: '中文' })).toBeNull()
-    expect(b.setLocale).not.toHaveBeenCalled()
-  })
-
-  it('reflects a store locale change in the trigger label (unknown id falls back to the id)', () => {
-    const b = mount({ active: 'en' })
-    act(() => { b.store.actions.syncLocale('zh', LOCALES, 1) })
-    expect(screen.getByRole('button', { name: /中文/ })).toBeDefined()
-    act(() => { b.store.actions.syncLocale('fr', LOCALES, 2) })
-    expect(screen.getByRole('button', { name: /fr/ })).toBeDefined()
-  })
-
-  it('marks the appearance cube matching the preference and switches on click', () => {
-    const b = mount({ preference: 'dark' })
-    expect(pressed(/Dark/)).toBe('true')
-    expect(pressed(/Light/)).toBe('false')
-    expect(pressed(/System/)).toBe('false')
-    fireEvent.click(screen.getByRole('button', { name: /Light/ }))
-    expect(b.setTheme).toHaveBeenCalledWith('light')
-    // Selection follows the store mirror, not the click echo.
-    act(() => { b.store.actions.syncTheme('light', 1) })
-    expect(pressed(/Light/)).toBe('true')
-    expect(pressed(/Dark/)).toBe('false')
-  })
-})

+ 0 - 18
packages/client/ui-settings-general/tests/invariant.spec.ts

@@ -1,18 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { Context } from 'cordis'
-import * as GeneralInvariant from '@deepseek-ai/dsh-client-ui-settings-general/invariant'
-import InvariantService from '@deepseek-ai/dsh-invariants'
-
-describe('invariant companion', () => {
-  it('registers under the package name with an empty installer', async () => {
-    const ctx = new Context()
-    await ctx.plugin(InvariantService, { enabled: true })
-    await expect(ctx.plugin(GeneralInvariant).await()).resolves.toBeDefined()
-  })
-
-  it('node-half apply is a no-op host placeholder', async () => {
-    const { apply } = await import('@deepseek-ai/dsh-client-ui-settings-general')
-    apply()
-    expect(true).toBe(true) // reaching here without throw is the contract
-  })
-})

+ 0 - 56
packages/client/ui-settings-general/tests/store.spec.ts

@@ -1,56 +0,0 @@
-/** General settings store: snapshot-mirror actions and the revision guard. */
-import { describe, expect, it } from 'vitest'
-import { createGeneralSettingsStore } from '../src/client/store.ts'
-
-const LOCALES = [{ id: 'zh', label: '中文' }, { id: 'en', label: 'English' }]
-
-describe('createGeneralSettingsStore', () => {
-  it('init shape: empty mirrors with revisions at -1', () => {
-    const store = createGeneralSettingsStore().create()
-    expect(store.getSnapshot()).toEqual({
-      localeActive: '',
-      localeOptions: [],
-      localeRevision: -1,
-      themePreference: 'system',
-      themeRevision: -1,
-    })
-  })
-
-  it('syncLocale mirrors the snapshot and advances the revision', () => {
-    const store = createGeneralSettingsStore().create()
-    store.actions.syncLocale('zh', LOCALES, 0)
-    expect(store.getSnapshot().localeActive).toBe('zh')
-    expect(store.getSnapshot().localeOptions).toEqual(LOCALES)
-    expect(store.getSnapshot().localeRevision).toBe(0)
-
-    store.actions.syncLocale('en', LOCALES, 1)
-    expect(store.getSnapshot().localeActive).toBe('en')
-    expect(store.getSnapshot().localeRevision).toBe(1)
-  })
-
-  it('syncLocale revision guard drops stale and duplicate writes', () => {
-    const store = createGeneralSettingsStore().create()
-    store.actions.syncLocale('en', LOCALES, 5)
-    // Stale (lower) and duplicate (equal) revisions leave the mirror intact.
-    store.actions.syncLocale('zh', LOCALES, 4)
-    store.actions.syncLocale('zh', LOCALES, 5)
-    expect(store.getSnapshot().localeActive).toBe('en')
-    expect(store.getSnapshot().localeRevision).toBe(5)
-  })
-
-  it('syncTheme mirrors the preference and guards its revision independently', () => {
-    const store = createGeneralSettingsStore().create()
-    store.actions.syncTheme('dark', 0)
-    expect(store.getSnapshot().themePreference).toBe('dark')
-    expect(store.getSnapshot().themeRevision).toBe(0)
-
-    store.actions.syncTheme('light', 2)
-    expect(store.getSnapshot().themePreference).toBe('light')
-
-    // Stale theme write is dropped; the locale revision axis is untouched.
-    store.actions.syncTheme('system', 1)
-    expect(store.getSnapshot().themePreference).toBe('light')
-    expect(store.getSnapshot().themeRevision).toBe(2)
-    expect(store.getSnapshot().localeRevision).toBe(-1)
-  })
-})

+ 0 - 36
packages/client/ui-settings-general/tsconfig.json

@@ -1,36 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.client.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../ui-slots"
-    },
-    {
-      "path": "../ui-primitives"
-    },
-    {
-      "path": "../runtime"
-    },
-    {
-      "path": "../ui-settings"
-    },
-    {
-      "path": "../locale"
-    },
-    {
-      "path": "../ui-theme"
-    },
-    {
-      "path": "../../support/invariants"
-    }
-  ]
-}

+ 0 - 3
packages/client/ui-settings-general/tsdown.config.ts

@@ -1,3 +0,0 @@
-import { clientBundle } from '../tsdown.client.ts'
-
-export default clientBundle('@deepseek-ai/dsh-client-ui-settings-general', ['lib/types/index.js', 'lib/types/invariant.js'])

+ 0 - 3
packages/client/ui-settings-models/tsdown.config.ts

@@ -1,3 +0,0 @@
-import { clientBundle } from '../tsdown.client.ts'
-
-export default clientBundle('@deepseek-ai/dsh-client-ui-settings-models', ['lib/types/index.js', 'lib/types/invariant.js'])

+ 9 - 29
packages/client/ui-settings-general/src/client/GeneralSection.module.css → packages/client/ui-settings/src/client/GeneralSection.module.css

@@ -1,6 +1,7 @@
-/* General section rows (figma 501:29983 'Options'): four groups, 16px
- * vertical padding each, hairline separator under all but the last. The
- * shell's content column owns the outer horizontal padding. */
+/* General section rows (figma 501:29983 'Options'): stacked groups, 16px
+ * vertical padding each, hairline separator under all but the last child
+ * (feature-contributed rows carry their own row chrome and separators; the
+ * :last-child rule strips the trailing one wherever the column ends). */
 
 .section {
   display: flex;
@@ -8,6 +9,10 @@
   width: 100%;
 }
 
+.section > :last-child {
+  border-bottom: none;
+}
+
 /* Title + trailing control row (figma 'Setting-Cell': gap 8, pad 16/0). */
 .row {
   display: flex;
@@ -26,10 +31,6 @@
   border-bottom: 1px solid var(--dsw-alias-border-l2);
 }
 
-.last {
-  border-bottom: none;
-}
-
 /* Leading text column (figma 'Frame 2036083120': gap 4, pad-right 48). */
 .rowText {
   flex: 1;
@@ -79,7 +80,7 @@
   flex: none;
 }
 
-/* Cube rows share an 8px gap; cubes stretch to equal height. */
+/* Tool Call mode cubes share an 8px gap. */
 .cubeRow {
   display: flex;
   align-items: stretch;
@@ -102,27 +103,6 @@
   text-align: left;
 }
 
-/* Appearance cube (figma '.Selector Cube' 276x82 r16, pad 20/32, centered
- * icon-over-label column, gap 4). */
-.themeCube {
-  box-sizing: border-box;
-  width: 276px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  gap: 4px;
-  padding: 20px 32px;
-  border: 1px solid var(--dsw-alias-border-l2);
-  border-radius: 16px;
-  background: transparent;
-  font: inherit;
-  font-size: 14px;
-  line-height: 22px;
-  color: var(--dsw-alias-label-primary);
-  cursor: pointer;
-}
-
 /* Selected cube: #F5F6F7 fill + #ADB2B8 border (static token — the bluish-400
  * step has no alias-layer name). */
 .selected {

+ 51 - 0
packages/client/ui-settings/src/client/GeneralSection.tsx

@@ -0,0 +1,51 @@
+/**
+ * Shell-owned General section (figma 501:29983 'Options'): Permission and
+ * Tool Call skeleton rows, then the feature-contributed preference rows from
+ * the `settings.general.item` slot (locale → Language, ui-theme →
+ * Appearance). The section column stacks rows; each row draws its own
+ * internals and separator.
+ */
+import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
+import type { GeneralSectionComponentProps } from './contract/slots.ts'
+import css from './GeneralSection.module.css'
+
+/**
+ * Render the General section content column.
+ * @param props - composed slot props (contract/slots.ts).
+ * @returns the section element tree.
+ */
+export function GeneralSection({ t, renderSlot }: GeneralSectionComponentProps) {
+  return (
+    <div className={css.section}>
+      {/* Permission (skeleton): disabled selector pill. */}
+      <div className={css.row}>
+        <div className={css.rowText}>
+          <div className={css.title}>{t('permission.title')}</div>
+          <div className={css.desc}>{t('permission.desc')}</div>
+        </div>
+        <button type="button" className={css.selector} disabled>
+          {t('permission.value')}
+          <IconChevronDownOutline14 className={css.chevron} />
+        </button>
+      </div>
+
+      {/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */}
+      <div className={css.group}>
+        <div className={css.title}>{t('toolcall.title')}</div>
+        <div className={css.cubeRow}>
+          <div className={`${css.modeCube} ${css.selected}`}>
+            <div className={css.title}>{t('toolcall.schema.title')}</div>
+            <div className={css.desc}>{t('toolcall.schema.desc')}</div>
+          </div>
+          <div className={css.modeCube}>
+            <div className={css.title}>{t('toolcall.code.title')}</div>
+            <div className={css.desc}>{t('toolcall.code.desc')}</div>
+          </div>
+        </div>
+      </div>
+
+      {/* Feature-owned preference rows (Language, Appearance, …). */}
+      {renderSlot('settings.general.item', {})}
+    </div>
+  )
+}

+ 35 - 3
packages/client/ui-settings/src/client/contract/slots.ts

@@ -1,7 +1,11 @@
 /**
- * Settings shell slot contract: the shell occupies the sidebar-owned
- * `sidebar.settings` hole and declares the `settings.section` list slot that
- * section plugins (General, Models, …) contribute pages into.
+ * Settings shell slot contract. The shell occupies the sidebar-owned
+ * `sidebar.settings` hole, declares the `settings.section` list slot that
+ * feature plugins contribute top-level pages into, and ships the first
+ * section itself: General, whose `settings.general.item` list slot receives
+ * preference rows from the features that own them (locale → Language,
+ * ui-theme → Appearance). A feature owns its settings surface — adding a
+ * setting never means editing the shell.
  */
 import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 // Type-only: pulls ui-sidebar's SlotMap merge (the 'sidebar.settings' entry)
@@ -19,6 +23,16 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
      * re-render trigger). Sections render inside the panel content column.
      */
     'settings.section': { kind: 'list'; scope: 'root'; owner: SettingsSectionOwnerProps }
+    /**
+     * One preference row inside the General section, contributed by the
+     * feature plugin that owns the preference (locale → Language, ui-theme →
+     * Appearance). Options: `id` (row key), `order` (row position). Rows
+     * draw their own internals (row layout, separators via CSS); the section
+     * column only stacks them. NOTE: packages/client/locale and ui-theme
+     * repeat this entry verbatim (reference-cycle avoidance) — declaration
+     * merging enforces the copies stay identical; edit all three together.
+     */
+    'settings.general.item': { kind: 'list'; scope: 'root'; owner: { children?: never } }
   }
 }
 
@@ -60,3 +74,21 @@ export type SettingsRootInjected = {
  */
 export type SettingsRootComponentProps =
   PropsRuntime<'sidebar.settings'> & PropsRenderSlots<'settings.section'> & SettingsRootInjected
+
+/**
+ * Injected share of the shell-owned General section: the shell's own
+ * `settings` namespace translate function for the skeleton rows (Permission,
+ * Tool Call). Live preference rows arrive through the item slot with their
+ * own faces.
+ */
+export type GeneralSectionInjected = {
+  /** Translate a `settings` dictionary key to the active-locale text. */
+  t: (key: string) => string
+}
+
+/**
+ * Full component props of the shell-owned General section: the section owner
+ * share, the declared item render share, and the injected face.
+ */
+export type GeneralSectionComponentProps =
+  PropsRuntime<'settings.section'> & PropsRenderSlots<'settings.general.item'> & GeneralSectionInjected

+ 51 - 8
packages/client/ui-settings/src/client/index.ts

@@ -1,17 +1,24 @@
 /**
  * Settings shell plugin, browser half. Occupies the sidebar-owned
  * `sidebar.settings` hole with the trigger row + modal panel, declares the
- * `settings.section` list slot, and projects that ledger into the panel
- * navigation. Export discipline: packages/client/AGENTS.md.
+ * `settings.section` list slot, projects that ledger into the panel
+ * navigation, and ships the first section itself: General, which declares
+ * the `settings.general.item` slot that feature plugins contribute
+ * preference rows into. Export discipline: packages/client/AGENTS.md.
  */
 import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
 // Type-only: pulls the locale plugin's Context/Events merges (ctx.locale,
 // 'locale/change') into this program.
 import type {} from '@deepseek-ai/dsh-client-locale/client'
-import type { SettingsRootInjected } from './contract/slots.ts'
+import type { GeneralSectionInjected, SettingsRootInjected } from './contract/slots.ts'
 import { SettingsRoot } from './SettingsRoot.tsx'
+import { GeneralSection } from './GeneralSection.tsx'
+import { en, zh } from './locales.ts'
 
-export type { SettingsRootComponentProps, SettingsRootInjected, SettingsSectionOwnerProps } from './contract/slots.ts'
+export type {
+  GeneralSectionComponentProps, GeneralSectionInjected,
+  SettingsRootComponentProps, SettingsRootInjected, SettingsSectionOwnerProps,
+} from './contract/slots.ts'
 
 /**
  * Required services (cordis fiber inject). The target slot is declared by
@@ -22,18 +29,20 @@ export type { SettingsRootComponentProps, SettingsRootInjected, SettingsSectionO
 export const inject = ['slots', 'locale']
 
 /**
- * Register the settings shell into `sidebar.settings` once the declaration is
- * on the ledger.
+ * Register the settings shell into `sidebar.settings` and the shell-owned
+ * General section into `settings.section`, each once its declaration is on
+ * the ledger.
  * @param ctx - client root context.
  */
 export function apply(ctx: ClientContext): void {
   ctx.effect(() => {
     const disposers = [
-      ctx.locale.register('settings', 'zh', { trigger: '设置', title: '设置', close: '关闭' }),
-      ctx.locale.register('settings', 'en', { trigger: 'Settings', title: 'Settings', close: 'Close' }),
+      ctx.locale.register('settings', 'zh', zh),
+      ctx.locale.register('settings', 'en', en),
     ]
     return () => { for (const dispose of disposers) dispose() }
   }, 'ui-settings: shell copy dictionaries')
+
   const injected = (): SettingsRootInjected => ({
     translate: (ref) => {
       const colon = ref.indexOf(':')
@@ -73,4 +82,38 @@ export function apply(ctx: ClientContext): void {
       dispose?.()
     }
   }, 'ui-settings: shell registration')
+
+  // The shell's own General section: first page, declares the item slot the
+  // feature plugins (locale, ui-theme, …) contribute preference rows into.
+  // Same ledger-judged deferral; label re-registers on locale change.
+  const generalInjected = (): GeneralSectionInjected => ({
+    t: ctx.locale.bind('settings'),
+  })
+  ctx.effect(() => {
+    let dispose: (() => void) | undefined
+    const tryRegister = (): void => {
+      if (ctx.slots.spec('settings.section') === undefined) return
+      if (ctx.slots.entries('settings.section').some(e => e.component === GeneralSection)) return
+      dispose = ctx.slots.register({
+        name: 'settings.section',
+        id: 'general',
+        order: 0,
+        label: ctx.locale.bind('settings')('general.nav'),
+        children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
+        inject: generalInjected,
+      }, GeneralSection)
+    }
+    const offLocale = ctx.on('locale/change', () => {
+      dispose?.()
+      dispose = undefined
+      tryRegister()
+    })
+    const unsubscribe = ctx.slots.subscribe('settings.section', () => { tryRegister() })
+    tryRegister()
+    return () => {
+      offLocale()
+      unsubscribe()
+      dispose?.()
+    }
+  }, 'ui-settings: general section registration')
 }

+ 12 - 14
packages/client/ui-settings-general/src/client/locales.ts → packages/client/ui-settings/src/client/locales.ts

@@ -1,7 +1,9 @@
 /**
- * `settings.general` namespace dictionaries. Skeleton-row technical copy
+ * `settings` namespace dictionaries: shell chrome plus the shell-owned
+ * General section (nav label, skeleton rows). Skeleton-row technical copy
  * (Read only / Schema mode / Code mode and their descriptions) is shared
- * verbatim across locales per the Figma design.
+ * verbatim across locales per the Figma design. Feature-owned rows
+ * (Language, Appearance) ship their copy in their own packages.
  */
 import type { LocaleDict } from '@deepseek-ai/dsh-client-locale/client'
 
@@ -16,27 +18,23 @@ const SHARED = {
 /** Simplified Chinese dictionary. */
 export const zh: LocaleDict = {
   ...SHARED,
-  'nav': '通用设置',
+  'trigger': '设置',
+  'title': '设置',
+  'close': '关闭',
+  'general.nav': '通用设置',
   'permission.title': '权限',
   'permission.desc': '选择默认权限模式',
   'toolcall.title': '工具调用',
-  'language.title': '语言',
-  'appearance.title': '外观',
-  'appearance.light': '浅色',
-  'appearance.dark': '深色',
-  'appearance.system': '跟随系统',
 }
 
 /** English dictionary. */
 export const en: LocaleDict = {
   ...SHARED,
-  'nav': 'General',
+  'trigger': 'Settings',
+  'title': 'Settings',
+  'close': 'Close',
+  'general.nav': 'General',
   'permission.title': 'Permission',
   'permission.desc': 'Choose default permission mode',
   'toolcall.title': 'Tool Call',
-  'language.title': 'Language',
-  'appearance.title': 'Appearance',
-  'appearance.light': 'Light',
-  'appearance.dark': 'Dark',
-  'appearance.system': 'System',
 }

+ 77 - 4
packages/client/ui-settings/tests/apply.spec.ts

@@ -4,8 +4,9 @@ import { describe, expect, it, vi } from 'vitest'
 import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
 import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
 import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings/client'
-import type { SettingsRootInjected } from '@deepseek-ai/dsh-client-ui-settings/client'
+import type { GeneralSectionInjected, SettingsRootInjected } from '@deepseek-ai/dsh-client-ui-settings/client'
 import { SettingsRoot } from '../src/client/SettingsRoot.tsx'
+import { GeneralSection } from '../src/client/GeneralSection.tsx'
 
 async function bench() {
   const ctx = new Context()
@@ -77,11 +78,14 @@ describe('ui-settings apply', () => {
     declare(b.slots)
     await b.ctx.plugin({ inject: [...inject], apply }).await()
     const injected = injectedOf(b.slots)
-    expect(injected.sections()).toEqual([])
+    // The shell ships its own General section (order 0) — the ledger is never
+    // empty once apply settles.
+    expect(injected.sections()).toEqual([{ id: 'general', order: 0, label: '通用设置' }])
     b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null)
-    b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null)
+    b.slots.register({ name: 'settings.section', id: 'a', order: 5 } as never, () => null)
     expect(injected.sections()).toEqual([
-      { id: 'a', order: 0, label: '' },
+      { id: 'general', order: 0, label: '通用设置' },
+      { id: 'a', order: 5, label: '' },
       { id: 'z', order: 20, label: 'Z' },
     ])
     expect(injected.sectionsVersion()).toBe(b.slots.getVersion('settings.section'))
@@ -118,3 +122,72 @@ describe('ui-settings apply', () => {
     expect(b.slots.spec('settings.section')).toBeUndefined()
   })
 })
+
+describe('ui-settings general section', () => {
+  it('registers the shell-owned General entry and declares the item slot', async () => {
+    const b = await bench()
+    declare(b.slots)
+    await b.ctx.plugin({ inject: [...inject], apply }).await()
+    const entry = b.slots.entries('settings.section')[0]!
+    expect(entry.component).toBe(GeneralSection)
+    expect(entry.options).toEqual({ id: 'general', order: 0, label: '通用设置' })
+    expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
+    const injected = (entry.inject as () => GeneralSectionInjected)()
+    expect(injected.t('permission.title')).toBe('权限')
+  })
+
+  it('re-registers with fresh label text on locale change', async () => {
+    const b = await bench()
+    declare(b.slots)
+    await b.ctx.plugin({ inject: [...inject], apply }).await()
+    b.locale.setLocale('en')
+    expect(b.slots.entries('settings.section')[0]!.options.label).toBe('General')
+    b.locale.setLocale('zh')
+    expect(b.slots.entries('settings.section')[0]!.options.label).toBe('通用设置')
+  })
+
+  it('locale change while settings.section is undeclared stays a no-op', async () => {
+    const b = await bench()
+    // No sidebar.settings declaration: the shell never registers, so
+    // settings.section is never declared either.
+    await b.ctx.plugin({ inject: [...inject], apply }).await()
+    b.locale.setLocale('en')
+    expect(b.slots.entries('settings.section')).toHaveLength(0)
+    b.locale.setLocale('zh')
+  })
+
+  it('re-registers after an HMR collapse of the whole chain (stale disposer must not block)', async () => {
+    const b = await bench()
+    const redeclare = declare(b.slots)
+    await b.ctx.plugin({ inject: [...inject], apply }).await()
+    expect(b.slots.entries('settings.section')).toHaveLength(1)
+    // Root declarer unload: the cascade removes the shell entry, the
+    // settings.section declaration, and the General entry below it.
+    redeclare()
+    expect(b.slots.entries('settings.section')).toHaveLength(0)
+    expect(b.slots.spec('settings.general.item')).toBeUndefined()
+    declare(b.slots)
+    // Two deferral hops: the shell re-registers (re-declaring
+    // settings.section), then General re-registers into it.
+    await Promise.resolve()
+    await Promise.resolve()
+    const entry = b.slots.entries('settings.section')[0]!
+    expect(entry.component).toBe(GeneralSection)
+    expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
+    // The recovered registration still rides the locale path.
+    b.locale.setLocale('en')
+    expect(b.slots.entries('settings.section')[0]!.options.label).toBe('General')
+    b.locale.setLocale('zh')
+  })
+
+  it('removes the General entry and its item declaration on teardown', async () => {
+    const b = await bench()
+    declare(b.slots)
+    const fiber = b.ctx.plugin({ inject: [...inject], apply })
+    await fiber.await()
+    expect(b.slots.spec('settings.general.item')).toBeDefined()
+    await fiber.dispose()
+    expect(b.slots.entries('settings.section')).toHaveLength(0)
+    expect(b.slots.spec('settings.general.item')).toBeUndefined()
+  })
+})

+ 47 - 0
packages/client/ui-settings/tests/general-section.spec.tsx

@@ -0,0 +1,47 @@
+// @vitest-environment jsdom
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, render, screen } from '@testing-library/react'
+import type { GeneralSectionComponentProps } from '../src/client/contract/slots.ts'
+import { GeneralSection } from '../src/client/GeneralSection.tsx'
+import { en } from '../src/client/locales.ts'
+
+afterEach(cleanup)
+
+function mount() {
+  const renderSlot = vi.fn(
+    ((key: string) => <div data-testid={`slot-${key}`} />) as GeneralSectionComponentProps['renderSlot'],
+  )
+  const props: GeneralSectionComponentProps = {
+    t: (key) => en[key] ?? key,
+    renderSlot,
+  }
+  const view = render(<GeneralSection {...props} />)
+  return { view, renderSlot }
+}
+
+describe('GeneralSection', () => {
+  it('renders the Permission skeleton row with the disabled selector', () => {
+    mount()
+    expect(screen.getByText('Permission')).toBeTruthy()
+    expect(screen.getByText('Choose default permission mode')).toBeTruthy()
+    const selector = screen.getByRole('button', { name: /Read only/ }) as HTMLButtonElement
+    expect(selector.disabled).toBe(true)
+  })
+
+  it('renders the Tool Call skeleton cubes with schema pinned selected', () => {
+    mount()
+    expect(screen.getByText('Tool Call')).toBeTruthy()
+    const schema = screen.getByText('Schema mode')
+    const code = screen.getByText('Code mode')
+    expect(schema.parentElement!.className).toContain('selected')
+    expect(code.parentElement!.className).not.toContain('selected')
+    expect(screen.getByText('Traditional function calling — invoke tools one at a time')).toBeTruthy()
+    expect(screen.getByText('Chain multiple tools with code — multi-step orchestration')).toBeTruthy()
+  })
+
+  it('renders the feature-contributed item slot after the skeleton rows', () => {
+    const { renderSlot } = mount()
+    expect(renderSlot).toHaveBeenCalledWith('settings.general.item', {})
+    expect(screen.getByTestId('slot-settings.general.item')).toBeTruthy()
+  })
+})

+ 21 - 4
packages/client/ui-theme/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-client-ui-theme",
-  "description": "Theme plugin: ThemeService (light/dark/system preference, prefers-color-scheme resolution, theme/change snapshots; no DOM), --dsw-* token base stylesheets",
+  "description": "Theme plugin: ThemeService (light/dark/system preference, prefers-color-scheme resolution, theme/change snapshots; no DOM), --dsw-* token base stylesheets; registers the Appearance settings row",
   "version": "0.0.1",
   "private": true,
   "type": "module",
@@ -24,18 +24,32 @@
     "./package.json": "./package.json"
   },
   "dshClient": {
-    "inject": [],
+    "inject": [
+      "@deepseek-ai/dsh-client-runtime",
+      "@deepseek-ai/dsh-client-locale"
+    ],
     "platform": "web",
     "immediately": true
   },
   "license": "BSD-3-Clause",
   "peerDependencies": {
+    "@deepseek-ai/dsh-client-locale": "^0.0.1",
+    "@deepseek-ai/dsh-client-runtime": "^0.0.1",
+    "@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
+    "@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
     "@deepseek-ai/dsh-invariants": "^0.0.1",
-    "cordis": "^4.0.0-rc.7"
+    "cordis": "^4.0.0-rc.7",
+    "react": "^18.2.0"
   },
   "devDependencies": {
+    "@deepseek-ai/dsh-client-locale": "workspace:^",
+    "@deepseek-ai/dsh-client-runtime": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "cordis": "^4.0.0-rc.7"
+    "@types/react": "~18.3.1",
+    "cordis": "^4.0.0-rc.7",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",
@@ -48,5 +62,8 @@
   "scripts": {
     "bundle": "tsdown",
     "watch": "tsdown --watch"
+  },
+  "dependencies": {
+    "clsx": "^2.0.0"
   }
 }

+ 51 - 0
packages/client/ui-theme/src/client/AppearanceRow.module.css

@@ -0,0 +1,51 @@
+/* Appearance row (figma 'Frame 2117131228': title + cube row, column gap 8,
+ * pad 16/0, hairline separator; the section column strips it when last). */
+
+.group {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 16px 0;
+  border-bottom: 1px solid var(--dsw-alias-border-l2);
+}
+
+.title {
+  font-size: 14px;
+  font-weight: 400;
+  line-height: 22px;
+  color: var(--dsw-alias-label-primary);
+}
+
+.cubeRow {
+  display: flex;
+  align-items: stretch;
+  gap: 8px;
+}
+
+/* Appearance cube (figma '.Selector Cube' 276x82 r16, pad 20/32, centered
+ * icon-over-label column, gap 4). */
+.themeCube {
+  box-sizing: border-box;
+  width: 276px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  padding: 20px 32px;
+  border: 1px solid var(--dsw-alias-border-l2);
+  border-radius: 16px;
+  background: transparent;
+  font: inherit;
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--dsw-alias-label-primary);
+  cursor: pointer;
+}
+
+/* Selected cube: #F5F6F7 fill + #ADB2B8 border (static token — the bluish-400
+ * step has no alias-layer name). */
+.selected {
+  background: var(--dsw-alias-bg-module-platform);
+  border-color: var(--dsw-static-neutral-bluish-400);
+}

+ 63 - 0
packages/client/ui-theme/src/client/AppearanceRow.tsx

@@ -0,0 +1,63 @@
+/**
+ * Appearance preference row registered into the General section item slot
+ * (figma 501:30012 'Frame 2117131228'): title + three preference cubes.
+ * Registered by this package — the theme feature owns its own settings
+ * surface. Selection follows the persisted preference, never the resolved
+ * active theme.
+ */
+import clsx from 'clsx'
+import {
+  IconDarkOutline16, IconFollowsystemOutline16, IconLightOutline16,
+} from '@deepseek-ai/dsh-client-ui-primitives'
+import type { PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
+import type { ThemePreference } from './index.ts'
+import type {} from './settings-contract.ts'
+import type { createAppearanceRowStore } from './settings-store.ts'
+import css from './AppearanceRow.module.css'
+
+/** Injected business face: namespace-bound translate + the preference write. */
+export interface AppearanceRowInjected {
+  /** Translate a `settings.theme` dictionary key to the active-locale text. */
+  t: (key: string) => string
+  /** Switch the theme preference. */
+  setTheme: (id: ThemePreference) => void
+}
+
+/** Full component props: runtime share + store share + injected face. */
+export type AppearanceRowComponentProps =
+  PropsRuntime<'settings.general.item'> & PropsStore<ReturnType<typeof createAppearanceRowStore>> & AppearanceRowInjected
+
+/** Cube order and icons (figma 501:30015-30017: Light, Dark, System). */
+const CUBES: readonly { id: ThemePreference; labelKey: string; Icon: typeof IconLightOutline16 }[] = [
+  { id: 'light', labelKey: 'appearance.light', Icon: IconLightOutline16 },
+  { id: 'dark', labelKey: 'appearance.dark', Icon: IconDarkOutline16 },
+  { id: 'system', labelKey: 'appearance.system', Icon: IconFollowsystemOutline16 },
+]
+
+/**
+ * Render the Appearance row.
+ * @param props - composed slot props.
+ * @returns the row element tree.
+ */
+export function AppearanceRow({ t, setTheme, useStore }: AppearanceRowComponentProps) {
+  const preference = useStore(s => s.preference)
+  return (
+    <div className={css.group}>
+      <div className={css.title}>{t('appearance.title')}</div>
+      <div className={css.cubeRow}>
+        {CUBES.map(({ id, labelKey, Icon }) => (
+          <button
+            key={id}
+            type="button"
+            className={clsx(css.themeCube, preference === id && css.selected)}
+            aria-pressed={preference === id}
+            onClick={() => { setTheme(id) }}
+          >
+            <Icon />
+            {t(labelKey)}
+          </button>
+        ))}
+      </div>
+    </div>
+  )
+}

+ 83 - 6
packages/client/ui-theme/src/client/index.ts

@@ -2,9 +2,24 @@
  * Browser theme registry over the `--dsw-*` token stylesheets. The service
  * owns the theme preference (light/dark/system), resolves `system` through
  * `prefers-color-scheme`, and publishes immutable snapshots; it never touches
- * the DOM — ui-layout's presenter consumes the resolved snapshot.
+ * the DOM — ui-layout's presenter consumes the resolved snapshot. The plugin
+ * also registers the Appearance preference row into the settings General
+ * section — the theme feature owns its own settings surface.
  */
 import type { Context } from 'cordis'
+import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots'
+import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
+// Type-only: pulls the locale plugin's Context merge (ctx.locale).
+import type {} from '@deepseek-ai/dsh-client-locale/client'
+import type { AppearanceRowInjected } from './AppearanceRow.tsx'
+import { AppearanceRow } from './AppearanceRow.tsx'
+import { createAppearanceRowStore } from './settings-store.ts'
+
+export type { AppearanceRowComponentProps, AppearanceRowInjected } from './AppearanceRow.tsx'
+export type { AppearanceRowState } from './settings-store.ts'
+
+/** Namespace owning this feature's settings-row copy. */
+export const SETTINGS_NS = 'settings.theme'
 
 /** Theme token dictionary: --dsw-alias-* overrides keyed by variable name. */
 export type ThemeTokens = Record<string, string>
@@ -200,13 +215,75 @@ function persistPreference(preference: ThemePreference): void {
   }
 }
 
-/** Required services (none; the loader passes the export surface as an object plugin). */
-export const inject: string[] = []
+/** Required services: slots + locale (the feature registers its own settings row with localized copy). */
+export const inject = ['slots', 'locale']
 
 /**
- * Client plugin body: provide the theme service.
+ * Client plugin body: provide the theme service and register the
+ * feature-owned Appearance preference row into the General section's item
+ * slot (a feature owns its settings surface).
  * @param ctx - client cordis context.
  */
-export function apply(ctx: Context): void {
-  ctx.provide('theme', new ThemeService(ctx))
+export function apply(ctx: ClientContext): void {
+  const theme = new ThemeService(ctx)
+  ctx.provide('theme', theme)
+
+  ctx.effect(() => {
+    const disposers = [
+      ctx.locale.register(SETTINGS_NS, 'zh', {
+        'appearance.title': '外观',
+        'appearance.light': '浅色',
+        'appearance.dark': '深色',
+        'appearance.system': '跟随系统',
+      }),
+      ctx.locale.register(SETTINGS_NS, 'en', {
+        'appearance.title': 'Appearance',
+        'appearance.light': 'Light',
+        'appearance.dark': 'Dark',
+        'appearance.system': 'System',
+      }),
+    ]
+    return () => { for (const dispose of disposers) dispose() }
+  }, 'ui-theme: settings row dictionaries')
+
+  const store = createAppearanceRowStore()
+  let bound: BoundActions<typeof store> | undefined
+  const sync = (snapshot: ThemeSnapshot): void => {
+    bound?.sync(snapshot.preference, snapshot.revision)
+  }
+  ctx.on('theme/change', sync)
+  const injected = (actions: BoundActions<typeof store>): AppearanceRowInjected => {
+    bound = actions
+    // Re-sync from the getter so no event is lost between registration and
+    // first render (the store's revision guard drops stale duplicates).
+    sync(theme.getTheme())
+    return {
+      t: ctx.locale.bind(SETTINGS_NS),
+      setTheme: (id) => { theme.setTheme(id) },
+    }
+  }
+  // Declaration-aware registration; the LEDGER is the has-registered judge
+  // (not a local flag): after an HMR collapse re-declares the slot, the
+  // cascade already removed our entry, and a stale disposer must not block
+  // the re-registration.
+  ctx.effect(() => {
+    let dispose: (() => void) | undefined
+    const tryRegister = (): void => {
+      if (ctx.slots.spec('settings.general.item') === undefined) return
+      if (ctx.slots.entries('settings.general.item').some(e => e.component === AppearanceRow)) return
+      dispose = ctx.slots.register({
+        name: 'settings.general.item',
+        id: 'appearance',
+        order: 10,
+        store,
+        inject: injected,
+      }, AppearanceRow)
+    }
+    const unsubscribe = ctx.slots.subscribe('settings.general.item', () => { tryRegister() })
+    tryRegister()
+    return () => {
+      unsubscribe()
+      dispose?.()
+    }
+  }, 'ui-theme: appearance settings row registration')
 }

+ 17 - 0
packages/client/ui-theme/src/client/settings-contract.ts

@@ -0,0 +1,17 @@
+/**
+ * Settings-surface slot merge consumed by this package's Appearance row. The
+ * AUTHORITATIVE home for 'settings.general.item' is the ui-settings contract
+ * (declaring is claiming: the shell's General entry declares the slot); this
+ * file repeats the entry verbatim because the settings shell sits above the
+ * feature layer, so importing its types from here would invert the layering.
+ * TypeScript declaration merging rejects diverging duplicates, so every
+ * program that sees both copies enforces identity.
+ */
+declare module '@deepseek-ai/dsh-client-ui-slots' {
+  interface SlotMap {
+    /** One preference row inside the General section (duplicate-identical merge; authority: ui-settings contract). */
+    'settings.general.item': { kind: 'list'; scope: 'root'; owner: { children?: never } }
+  }
+}
+
+export {}

+ 37 - 0
packages/client/ui-theme/src/client/settings-store.ts

@@ -0,0 +1,37 @@
+/**
+ * Appearance row slot store: a mirror of the theme service snapshot. The
+ * plugin's apply-world change listener is the only writer; the row component
+ * reads via props.useStore.
+ */
+import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client'
+import type { ThemePreference } from './index.ts'
+
+/** Store state mirrored from the theme snapshot. */
+export interface AppearanceRowState {
+  /** Persisted preference (selection state reads this, never the resolved active theme). */
+  preference: ThemePreference
+  /** Service revision; -1 until first sync so revision 0 lands as a change. */
+  revision: number
+}
+
+/** Declared action shape giving the exported factory a stable return type. */
+type AppearanceRowActions = {
+  sync: (draft: AppearanceRowState, preference: ThemePreference, revision: number) => void
+}
+
+/**
+ * Declares the Appearance row state and write surface.
+ * @returns the store handle.
+ */
+export function createAppearanceRowStore(): EngineStoreHandle<AppearanceRowState, AppearanceRowActions> {
+  return defineStore({
+    init: (): AppearanceRowState => ({ preference: 'system', revision: -1 }),
+    actions: {
+      sync: (d, preference: ThemePreference, revision: number) => {
+        if (revision <= d.revision) return
+        d.preference = preference
+        d.revision = revision
+      },
+    },
+  })
+}

+ 6 - 0
packages/client/ui-theme/src/css-modules.d.ts

@@ -0,0 +1,6 @@
+declare module '*.module.css' {
+  const classes: Record<string, string>
+  export default classes
+}
+
+declare module '*.css'

+ 8 - 2
packages/client/ui-theme/tests/invariant.spec.ts

@@ -4,6 +4,8 @@ import { Context } from 'cordis'
 import { apply as nodeApply } from '@deepseek-ai/dsh-client-ui-theme'
 import { apply as clientApply, inject, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client'
 import * as ThemeInvariant from '@deepseek-ai/dsh-client-ui-theme/invariant'
+import { apply as localeApply } from '@deepseek-ai/dsh-client-locale/client'
+import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
 import InvariantService from '@deepseek-ai/dsh-invariants'
 
 describe('invariant companion', () => {
@@ -18,9 +20,13 @@ describe('invariant companion', () => {
     expect(true).toBe(true) // reaching here without throw is the contract
   })
 
-  it('client apply provides ctx.theme with no service prerequisites', async () => {
-    expect(inject).toEqual([])
+  it('client apply provides ctx.theme over the slots/locale edges', async () => {
+    // The feature registers its own Appearance settings row with localized
+    // copy, hence the slots + locale edges.
+    expect(inject).toEqual(['slots', 'locale'])
     const ctx = new Context()
+    new SlotsService(ctx)
+    await ctx.plugin({ inject: ['slots'], apply: localeApply }).await()
     await ctx.plugin({ inject, apply: clientApply }).await()
     expect(ctx.get('theme')).toBeInstanceOf(ThemeService)
   })

+ 12 - 0
packages/client/ui-theme/tsconfig.json

@@ -8,6 +8,18 @@
     "src"
   ],
   "references": [
+    {
+      "path": "../locale"
+    },
+    {
+      "path": "../runtime"
+    },
+    {
+      "path": "../ui-primitives"
+    },
+    {
+      "path": "../ui-slots"
+    },
     {
       "path": "../../../vendor/cordis"
     },

+ 58 - 57
pnpm-lock.yaml

@@ -140,18 +140,15 @@ importers:
       '@deepseek-ai/dsh-client-ui-layout':
         specifier: workspace:^
         version: link:../../packages/client/ui-layout
+      '@deepseek-ai/dsh-client-ui-models':
+        specifier: workspace:^
+        version: link:../../packages/client/ui-models
       '@deepseek-ai/dsh-client-ui-question':
         specifier: workspace:^
         version: link:../../packages/client/ui-question
       '@deepseek-ai/dsh-client-ui-settings':
         specifier: workspace:^
         version: link:../../packages/client/ui-settings
-      '@deepseek-ai/dsh-client-ui-settings-general':
-        specifier: workspace:^
-        version: link:../../packages/client/ui-settings-general
-      '@deepseek-ai/dsh-client-ui-settings-models':
-        specifier: workspace:^
-        version: link:../../packages/client/ui-settings-models
       '@deepseek-ai/dsh-client-ui-sidebar':
         specifier: workspace:^
         version: link:../../packages/client/ui-sidebar
@@ -750,13 +747,32 @@ importers:
         version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@vendor+loader)
 
   packages/client/locale:
+    dependencies:
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
     devDependencies:
+      '@deepseek-ai/dsh-client-runtime':
+        specifier: workspace:^
+        version: link:../runtime
+      '@deepseek-ai/dsh-client-ui-primitives':
+        specifier: workspace:^
+        version: link:../ui-primitives
+      '@deepseek-ai/dsh-client-ui-slots':
+        specifier: workspace:^
+        version: link:../ui-slots
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../support/invariants
+      '@types/react':
+        specifier: ~18.3.1
+        version: 18.3.31
       cordis:
         specifier: ^4.0.0-rc.7
         version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
 
   packages/client/modules:
     devDependencies:
@@ -865,6 +881,33 @@ importers:
         specifier: ^18.2.0
         version: 18.3.1
 
+  packages/client/ui-models:
+    devDependencies:
+      '@deepseek-ai/dsh-client-locale':
+        specifier: workspace:^
+        version: link:../locale
+      '@deepseek-ai/dsh-client-runtime':
+        specifier: workspace:^
+        version: link:../runtime
+      '@deepseek-ai/dsh-client-ui-settings':
+        specifier: workspace:^
+        version: link:../ui-settings
+      '@deepseek-ai/dsh-client-ui-slots':
+        specifier: workspace:^
+        version: link:../ui-slots
+      '@deepseek-ai/dsh-invariants':
+        specifier: workspace:^
+        version: link:../../support/invariants
+      '@types/react':
+        specifier: ~18.3.1
+        version: 18.3.31
+      cordis:
+        specifier: ^4.0.0-rc.7
+        version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+
   packages/client/ui-primitives:
     dependencies:
       clsx:
@@ -979,30 +1022,24 @@ importers:
         specifier: ^18.2.0
         version: 18.3.1
 
-  packages/client/ui-settings-general:
+  packages/client/ui-sidebar:
     dependencies:
       clsx:
         specifier: ^2.0.0
         version: 2.1.1
     devDependencies:
-      '@deepseek-ai/dsh-client-locale':
-        specifier: workspace:^
-        version: link:../locale
       '@deepseek-ai/dsh-client-runtime':
         specifier: workspace:^
         version: link:../runtime
+      '@deepseek-ai/dsh-client-ui-layout':
+        specifier: workspace:^
+        version: link:../ui-layout
       '@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-client-ui-theme':
-        specifier: workspace:^
-        version: link:../ui-theme
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../support/invariants
@@ -1016,20 +1053,8 @@ importers:
         specifier: ^18.2.0
         version: 18.3.1
 
-  packages/client/ui-settings-models:
+  packages/client/ui-slots:
     devDependencies:
-      '@deepseek-ai/dsh-client-locale':
-        specifier: workspace:^
-        version: link:../locale
-      '@deepseek-ai/dsh-client-runtime':
-        specifier: workspace:^
-        version: link:../runtime
-      '@deepseek-ai/dsh-client-ui-settings':
-        specifier: workspace:^
-        version: link:../ui-settings
-      '@deepseek-ai/dsh-client-ui-slots':
-        specifier: workspace:^
-        version: link:../ui-slots
       '@deepseek-ai/dsh-invariants':
         specifier: workspace:^
         version: link:../../support/invariants
@@ -1039,22 +1064,19 @@ importers:
       cordis:
         specifier: ^4.0.0-rc.7
         version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
 
-  packages/client/ui-sidebar:
+  packages/client/ui-theme:
     dependencies:
       clsx:
         specifier: ^2.0.0
         version: 2.1.1
     devDependencies:
+      '@deepseek-ai/dsh-client-locale':
+        specifier: workspace:^
+        version: link:../locale
       '@deepseek-ai/dsh-client-runtime':
         specifier: workspace:^
         version: link:../runtime
-      '@deepseek-ai/dsh-client-ui-layout':
-        specifier: workspace:^
-        version: link:../ui-layout
       '@deepseek-ai/dsh-client-ui-primitives':
         specifier: workspace:^
         version: link:../ui-primitives
@@ -1074,27 +1096,6 @@ importers:
         specifier: ^18.2.0
         version: 18.3.1
 
-  packages/client/ui-slots:
-    devDependencies:
-      '@deepseek-ai/dsh-invariants':
-        specifier: workspace:^
-        version: link:../../support/invariants
-      '@types/react':
-        specifier: ~18.3.1
-        version: 18.3.31
-      cordis:
-        specifier: ^4.0.0-rc.7
-        version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
-
-  packages/client/ui-theme:
-    devDependencies:
-      '@deepseek-ai/dsh-invariants':
-        specifier: workspace:^
-        version: link:../../support/invariants
-      cordis:
-        specifier: ^4.0.0-rc.7
-        version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
-
   packages/client/ui-trajectory:
     devDependencies:
       '@deepseek-ai/dsh-client-runtime':

+ 1 - 2
scripts/verify-package-readme-model-experience.ts

@@ -60,8 +60,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
   'packages/client/ui-theme': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
   'packages/client/ui-settings': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
-  'packages/client/ui-settings-general': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
-  'packages/client/ui-settings-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
+  'packages/client/ui-models': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
   'packages/client/locale': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
   'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
   'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' },

+ 1 - 2
tsconfig.base.json

@@ -116,8 +116,7 @@
       "@deepseek-ai/dsh-client-ui-workspace": ["./packages/client/ui-workspace/src"],
       "@deepseek-ai/dsh-client-ui-theme": ["./packages/client/ui-theme/src"],
       "@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-models": ["./packages/client/ui-models/src"],
       "@deepseek-ai/dsh-client-locale": ["./packages/client/locale/src"],
       "@deepseek-ai/dsh-client-web": ["./packages/client/web/src"],
       "@deepseek-ai/dsh-*": [

+ 1 - 2
tsconfig.client.json

@@ -39,8 +39,7 @@
     { "path": "./packages/client/ui-trajectory" },
     { "path": "./packages/client/ui-theme" },
     { "path": "./packages/client/ui-settings" },
-    { "path": "./packages/client/ui-settings-general" },
-    { "path": "./packages/client/ui-settings-models" },
+    { "path": "./packages/client/ui-models" },
     { "path": "./packages/client/locale" },
     { "path": "./packages/client/web" },
     { "path": "./apps/web" }