From 23a60ade67215b2ad6da00e24921fba8dafda4b9 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 26 Jul 2026 02:51:36 +0800 Subject: [PATCH] refactor(gui): features register their own settings surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ...2026-07-25-client-settings-locale-theme.md | 42 ++++-- ...6-07-25-client-settings-locale-theme.zh.md | 44 +++--- apps/cli/cordis.yml | 7 +- apps/cli/package.json | 3 +- apps/cli/tsconfig.json | 5 +- apps/web/tests/session-title.snapshot.ts | 3 +- apps/web/tests/workspace-flow.snapshot.ts | 3 +- packages/client/locale/package.json | 22 ++- .../locale/src/client/LanguageRow.module.css | 47 ++++++ .../client/locale/src/client/LanguageRow.tsx | 68 +++++++++ packages/client/locale/src/client/index.ts | 72 ++++++++- .../locale/src/client/settings-contract.ts | 18 +++ .../locale/src/client/settings-store.ts | 47 ++++++ .../src/css-modules.d.ts | 0 .../client/locale/tests/invariant.spec.ts | 6 +- packages/client/locale/tsconfig.json | 9 ++ .../README.md | 2 +- .../package.json | 4 +- .../src/client/ModelsSection.tsx | 0 .../src/client/index.ts | 4 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/apply.spec.ts | 4 +- .../tests/invariant.spec.ts | 4 +- .../tsconfig.json | 0 packages/client/ui-models/tsdown.config.ts | 3 + packages/client/ui-settings-general/README.md | 15 -- .../client/ui-settings-general/package.json | 70 --------- .../src/client/GeneralSection.tsx | 118 --------------- .../src/client/contract.ts | 66 -------- .../ui-settings-general/src/client/index.ts | 117 --------------- .../ui-settings-general/src/client/store.ts | 43 ------ .../client/ui-settings-general/src/index.ts | 4 - .../ui-settings-general/src/invariant.ts | 32 ---- .../ui-settings-general/tests/apply.spec.ts | 139 ----------------- .../tests/general-section.spec.tsx | 112 -------------- .../tests/invariant.spec.ts | 18 --- .../ui-settings-general/tests/store.spec.ts | 56 ------- .../client/ui-settings-general/tsconfig.json | 36 ----- .../ui-settings-general/tsdown.config.ts | 3 - .../ui-settings-models/tsdown.config.ts | 3 - .../src/client/GeneralSection.module.css | 38 ++--- .../ui-settings/src/client/GeneralSection.tsx | 51 +++++++ .../ui-settings/src/client/contract/slots.ts | 38 ++++- .../client/ui-settings/src/client/index.ts | 59 +++++++- .../src/client/locales.ts | 26 ++-- .../client/ui-settings/tests/apply.spec.ts | 81 +++++++++- .../tests/general-section.spec.tsx | 47 ++++++ packages/client/ui-theme/package.json | 25 +++- .../src/client/AppearanceRow.module.css | 51 +++++++ .../ui-theme/src/client/AppearanceRow.tsx | 63 ++++++++ packages/client/ui-theme/src/client/index.ts | 89 ++++++++++- .../ui-theme/src/client/settings-contract.ts | 17 +++ .../ui-theme/src/client/settings-store.ts | 37 +++++ packages/client/ui-theme/src/css-modules.d.ts | 6 + .../client/ui-theme/tests/invariant.spec.ts | 10 +- packages/client/ui-theme/tsconfig.json | 12 ++ pnpm-lock.yaml | 141 +++++++++--------- .../verify-package-readme-model-experience.ts | 3 +- tsconfig.base.json | 3 +- tsconfig.client.json | 3 +- 62 files changed, 1008 insertions(+), 1049 deletions(-) create mode 100644 packages/client/locale/src/client/LanguageRow.module.css create mode 100644 packages/client/locale/src/client/LanguageRow.tsx create mode 100644 packages/client/locale/src/client/settings-contract.ts create mode 100644 packages/client/locale/src/client/settings-store.ts rename packages/client/{ui-settings-general => locale}/src/css-modules.d.ts (100%) rename packages/client/{ui-settings-models => ui-models}/README.md (92%) rename packages/client/{ui-settings-models => ui-models}/package.json (89%) rename packages/client/{ui-settings-models => ui-models}/src/client/ModelsSection.tsx (100%) rename packages/client/{ui-settings-models => ui-models}/src/client/index.ts (96%) rename packages/client/{ui-settings-models => ui-models}/src/css-modules.d.ts (100%) rename packages/client/{ui-settings-models => ui-models}/src/index.ts (100%) rename packages/client/{ui-settings-models => ui-models}/src/invariant.ts (81%) rename packages/client/{ui-settings-models => ui-models}/tests/apply.spec.ts (96%) rename packages/client/{ui-settings-models => ui-models}/tests/invariant.spec.ts (82%) rename packages/client/{ui-settings-models => ui-models}/tsconfig.json (100%) create mode 100644 packages/client/ui-models/tsdown.config.ts delete mode 100644 packages/client/ui-settings-general/README.md delete mode 100644 packages/client/ui-settings-general/package.json delete mode 100644 packages/client/ui-settings-general/src/client/GeneralSection.tsx delete mode 100644 packages/client/ui-settings-general/src/client/contract.ts delete mode 100644 packages/client/ui-settings-general/src/client/index.ts delete mode 100644 packages/client/ui-settings-general/src/client/store.ts delete mode 100644 packages/client/ui-settings-general/src/index.ts delete mode 100644 packages/client/ui-settings-general/src/invariant.ts delete mode 100644 packages/client/ui-settings-general/tests/apply.spec.ts delete mode 100644 packages/client/ui-settings-general/tests/general-section.spec.tsx delete mode 100644 packages/client/ui-settings-general/tests/invariant.spec.ts delete mode 100644 packages/client/ui-settings-general/tests/store.spec.ts delete mode 100644 packages/client/ui-settings-general/tsconfig.json delete mode 100644 packages/client/ui-settings-general/tsdown.config.ts delete mode 100644 packages/client/ui-settings-models/tsdown.config.ts rename packages/client/{ui-settings-general => ui-settings}/src/client/GeneralSection.module.css (75%) create mode 100644 packages/client/ui-settings/src/client/GeneralSection.tsx rename packages/client/{ui-settings-general => ui-settings}/src/client/locales.ts (64%) create mode 100644 packages/client/ui-settings/tests/general-section.spec.tsx create mode 100644 packages/client/ui-theme/src/client/AppearanceRow.module.css create mode 100644 packages/client/ui-theme/src/client/AppearanceRow.tsx create mode 100644 packages/client/ui-theme/src/client/settings-contract.ts create mode 100644 packages/client/ui-theme/src/client/settings-store.ts create mode 100644 packages/client/ui-theme/src/css-modules.d.ts diff --git a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md index e1245a9e1f..0dfcb4ea90 100644 --- a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md +++ b/.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. diff --git a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.zh.md b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.zh.md index 195b2e5ffa..dfe93a0ca1 100644 --- a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.zh.md +++ b/.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 后残留。 diff --git a/apps/cli/cordis.yml b/apps/cli/cordis.yml index 8412a313b4..39803aa603 100644 --- a/apps/cli/cordis.yml +++ b/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' diff --git a/apps/cli/package.json b/apps/cli/package.json index 74a6e417e1..ddf8a08a87 100644 --- a/apps/cli/package.json +++ b/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:^", diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 7614ae2e31..3000fd1f8d 100644 --- a/apps/cli/tsconfig.json +++ b/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" diff --git a/apps/web/tests/session-title.snapshot.ts b/apps/web/tests/session-title.snapshot.ts index b8ee3408a3..35356fa0b3 100644 --- a/apps/web/tests/session-title.snapshot.ts +++ b/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'] }, diff --git a/apps/web/tests/workspace-flow.snapshot.ts b/apps/web/tests/workspace-flow.snapshot.ts index e232d8e123..738357a551 100644 --- a/apps/web/tests/workspace-flow.snapshot.ts +++ b/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', diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index 340efe042e..d53d2e4269 100644 --- a/packages/client/locale/package.json +++ b/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" } } diff --git a/packages/client/locale/src/client/LanguageRow.module.css b/packages/client/locale/src/client/LanguageRow.module.css new file mode 100644 index 0000000000..f17a67d279 --- /dev/null +++ b/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; +} diff --git a/packages/client/locale/src/client/LanguageRow.tsx b/packages/client/locale/src/client/LanguageRow.tsx new file mode 100644 index 0000000000..a824bc6752 --- /dev/null +++ b/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> & 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 ( +
+
+
{t('language.title')}
+
+ { setOpen(false) }} + items={options.map(o => ({ id: o.id, label: o.label }))} + selectedId={active} + onSelect={(id) => { + setLocale(id) + setOpen(false) + }} + align="end" + portal + anchor={( + + )} + /> +
+ ) +} diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index 35f29a0e52..444594f9b8 100644 --- a/packages/client/locale/src/client/index.ts +++ b/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 @@ -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 | 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): 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') } diff --git a/packages/client/locale/src/client/settings-contract.ts b/packages/client/locale/src/client/settings-contract.ts new file mode 100644 index 0000000000..add314122c --- /dev/null +++ b/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 {} diff --git a/packages/client/locale/src/client/settings-store.ts b/packages/client/locale/src/client/settings-store.ts new file mode 100644 index 0000000000..485fd409f0 --- /dev/null +++ b/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 { + 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 + }, + }, + }) +} diff --git a/packages/client/ui-settings-general/src/css-modules.d.ts b/packages/client/locale/src/css-modules.d.ts similarity index 100% rename from packages/client/ui-settings-general/src/css-modules.d.ts rename to packages/client/locale/src/css-modules.d.ts diff --git a/packages/client/locale/tests/invariant.spec.ts b/packages/client/locale/tests/invariant.spec.ts index e9f4fdee36..fa62ca79f6 100644 --- a/packages/client/locale/tests/invariant.spec.ts +++ b/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) diff --git a/packages/client/locale/tsconfig.json b/packages/client/locale/tsconfig.json index 51f9171643..8585ba74ca 100644 --- a/packages/client/locale/tsconfig.json +++ b/packages/client/locale/tsconfig.json @@ -8,6 +8,15 @@ "src" ], "references": [ + { + "path": "../runtime" + }, + { + "path": "../ui-primitives" + }, + { + "path": "../ui-slots" + }, { "path": "../../../vendor/cordis" }, diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-models/README.md similarity index 92% rename from packages/client/ui-settings-models/README.md rename to packages/client/ui-models/README.md index c96e2843e4..b5f6c9bea9 100644 --- a/packages/client/ui-settings-models/README.md +++ b/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. diff --git a/packages/client/ui-settings-models/package.json b/packages/client/ui-models/package.json similarity index 89% rename from packages/client/ui-settings-models/package.json rename to packages/client/ui-models/package.json index da8fa8c18d..df8cba7c8b 100644 --- a/packages/client/ui-settings-models/package.json +++ b/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", diff --git a/packages/client/ui-settings-models/src/client/ModelsSection.tsx b/packages/client/ui-models/src/client/ModelsSection.tsx similarity index 100% rename from packages/client/ui-settings-models/src/client/ModelsSection.tsx rename to packages/client/ui-models/src/client/ModelsSection.tsx diff --git a/packages/client/ui-settings-models/src/client/index.ts b/packages/client/ui-models/src/client/index.ts similarity index 96% rename from packages/client/ui-settings-models/src/client/index.ts rename to packages/client/ui-models/src/client/index.ts index c35946fcc8..4f080ec928 100644 --- a/packages/client/ui-settings-models/src/client/index.ts +++ b/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') } diff --git a/packages/client/ui-settings-models/src/css-modules.d.ts b/packages/client/ui-models/src/css-modules.d.ts similarity index 100% rename from packages/client/ui-settings-models/src/css-modules.d.ts rename to packages/client/ui-models/src/css-modules.d.ts diff --git a/packages/client/ui-settings-models/src/index.ts b/packages/client/ui-models/src/index.ts similarity index 100% rename from packages/client/ui-settings-models/src/index.ts rename to packages/client/ui-models/src/index.ts diff --git a/packages/client/ui-settings-models/src/invariant.ts b/packages/client/ui-models/src/invariant.ts similarity index 81% rename from packages/client/ui-settings-models/src/invariant.ts rename to packages/client/ui-models/src/invariant.ts index 9ffdc668d7..c7c4996748 100644 --- a/packages/client/ui-settings-models/src/invariant.ts +++ b/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'] diff --git a/packages/client/ui-settings-models/tests/apply.spec.ts b/packages/client/ui-models/tests/apply.spec.ts similarity index 96% rename from packages/client/ui-settings-models/tests/apply.spec.ts rename to packages/client/ui-models/tests/apply.spec.ts index b7aa3cf2ab..1842000675 100644 --- a/packages/client/ui-settings-models/tests/apply.spec.ts +++ b/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']) }) diff --git a/packages/client/ui-settings-models/tests/invariant.spec.ts b/packages/client/ui-models/tests/invariant.spec.ts similarity index 82% rename from packages/client/ui-settings-models/tests/invariant.spec.ts rename to packages/client/ui-models/tests/invariant.spec.ts index 65c7c1094a..05fb52ee1b 100644 --- a/packages/client/ui-settings-models/tests/invariant.spec.ts +++ b/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 }) diff --git a/packages/client/ui-settings-models/tsconfig.json b/packages/client/ui-models/tsconfig.json similarity index 100% rename from packages/client/ui-settings-models/tsconfig.json rename to packages/client/ui-models/tsconfig.json diff --git a/packages/client/ui-models/tsdown.config.ts b/packages/client/ui-models/tsdown.config.ts new file mode 100644 index 0000000000..fc044b07d6 --- /dev/null +++ b/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']) diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md deleted file mode 100644 index 435dd8c1b4..0000000000 --- a/packages/client/ui-settings-general/README.md +++ /dev/null @@ -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. diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json deleted file mode 100644 index 9b3d83e472..0000000000 --- a/packages/client/ui-settings-general/package.json +++ /dev/null @@ -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" - ] -} diff --git a/packages/client/ui-settings-general/src/client/GeneralSection.tsx b/packages/client/ui-settings-general/src/client/GeneralSection.tsx deleted file mode 100644 index d2947c0435..0000000000 --- a/packages/client/ui-settings-general/src/client/GeneralSection.tsx +++ /dev/null @@ -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 ( -
- {/* Permission (skeleton): disabled selector pill. */} -
-
-
{t('permission.title')}
-
{t('permission.desc')}
-
- -
- - {/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */} -
-
{t('toolcall.title')}
-
-
-
{t('toolcall.schema.title')}
-
{t('toolcall.schema.desc')}
-
-
-
{t('toolcall.code.title')}
-
{t('toolcall.code.desc')}
-
-
-
- - {/* Language: selector pill opens the locale menu. */} -
-
-
{t('language.title')}
-
- { setLanguageOpen(false) }} - items={localeOptions.map(l => ({ id: l.id, label: l.label }))} - selectedId={localeActive} - onSelect={(id) => { - setLocale(id) - setLanguageOpen(false) - }} - align="end" - portal - anchor={( - - )} - /> -
- - {/* Appearance: three preference cubes; selection follows the persisted - * preference, never the resolved active theme. */} -
-
{t('appearance.title')}
-
- {THEME_CUBES.map(({ id, labelKey, Icon }) => ( - - ))} -
-
-
- ) -} diff --git a/packages/client/ui-settings-general/src/client/contract.ts b/packages/client/ui-settings-general/src/client/contract.ts deleted file mode 100644 index 912d925615..0000000000 --- a/packages/client/ui-settings-general/src/client/contract.ts +++ /dev/null @@ -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 - -/** - * 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 & GeneralSectionInjected diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts deleted file mode 100644 index 52655be7fa..0000000000 --- a/packages/client/ui-settings-general/src/client/index.ts +++ /dev/null @@ -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 | 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): 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') -} diff --git a/packages/client/ui-settings-general/src/client/store.ts b/packages/client/ui-settings-general/src/client/store.ts deleted file mode 100644 index 121e3f52a9..0000000000 --- a/packages/client/ui-settings-general/src/client/store.ts +++ /dev/null @@ -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 { - 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 - }, - }, - }) -} diff --git a/packages/client/ui-settings-general/src/index.ts b/packages/client/ui-settings-general/src/index.ts deleted file mode 100644 index 94b9bdf674..0000000000 --- a/packages/client/ui-settings-general/src/index.ts +++ /dev/null @@ -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 {} diff --git a/packages/client/ui-settings-general/src/invariant.ts b/packages/client/ui-settings-general/src/invariant.ts deleted file mode 100644 index a40cc3cc00..0000000000 --- a/packages/client/ui-settings-general/src/invariant.ts +++ /dev/null @@ -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 */ diff --git a/packages/client/ui-settings-general/tests/apply.spec.ts b/packages/client/ui-settings-general/tests/apply.spec.ts deleted file mode 100644 index 8654275831..0000000000 --- a/packages/client/ui-settings-general/tests/apply.spec.ts +++ /dev/null @@ -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 - 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') - }) -}) diff --git a/packages/client/ui-settings-general/tests/general-section.spec.tsx b/packages/client/ui-settings-general/tests/general-section.spec.tsx deleted file mode 100644 index e82ed0b1fc..0000000000 --- a/packages/client/ui-settings-general/tests/general-section.spec.tsx +++ /dev/null @@ -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( - { ids: [], byId: {}, current: undefined, intent: undefined, phase: 'ready' }) - return bindSnapshotSelector(store) -} -function emptyWorkspaces() { - const store = createSnapshotStore({ - 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() - 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') - }) -}) diff --git a/packages/client/ui-settings-general/tests/invariant.spec.ts b/packages/client/ui-settings-general/tests/invariant.spec.ts deleted file mode 100644 index 7b0527c0ff..0000000000 --- a/packages/client/ui-settings-general/tests/invariant.spec.ts +++ /dev/null @@ -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 - }) -}) diff --git a/packages/client/ui-settings-general/tests/store.spec.ts b/packages/client/ui-settings-general/tests/store.spec.ts deleted file mode 100644 index b291418471..0000000000 --- a/packages/client/ui-settings-general/tests/store.spec.ts +++ /dev/null @@ -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) - }) -}) diff --git a/packages/client/ui-settings-general/tsconfig.json b/packages/client/ui-settings-general/tsconfig.json deleted file mode 100644 index 9b2cc5b838..0000000000 --- a/packages/client/ui-settings-general/tsconfig.json +++ /dev/null @@ -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" - } - ] -} diff --git a/packages/client/ui-settings-general/tsdown.config.ts b/packages/client/ui-settings-general/tsdown.config.ts deleted file mode 100644 index bf67c4f10f..0000000000 --- a/packages/client/ui-settings-general/tsdown.config.ts +++ /dev/null @@ -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']) diff --git a/packages/client/ui-settings-models/tsdown.config.ts b/packages/client/ui-settings-models/tsdown.config.ts deleted file mode 100644 index 7a2688a097..0000000000 --- a/packages/client/ui-settings-models/tsdown.config.ts +++ /dev/null @@ -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']) diff --git a/packages/client/ui-settings-general/src/client/GeneralSection.module.css b/packages/client/ui-settings/src/client/GeneralSection.module.css similarity index 75% rename from packages/client/ui-settings-general/src/client/GeneralSection.module.css rename to packages/client/ui-settings/src/client/GeneralSection.module.css index dffafc419d..5e053a49c5 100644 --- a/packages/client/ui-settings-general/src/client/GeneralSection.module.css +++ b/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 { diff --git a/packages/client/ui-settings/src/client/GeneralSection.tsx b/packages/client/ui-settings/src/client/GeneralSection.tsx new file mode 100644 index 0000000000..31714a9238 --- /dev/null +++ b/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 ( +
+ {/* Permission (skeleton): disabled selector pill. */} +
+
+
{t('permission.title')}
+
{t('permission.desc')}
+
+ +
+ + {/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */} +
+
{t('toolcall.title')}
+
+
+
{t('toolcall.schema.title')}
+
{t('toolcall.schema.desc')}
+
+
+
{t('toolcall.code.title')}
+
{t('toolcall.code.desc')}
+
+
+
+ + {/* Feature-owned preference rows (Language, Appearance, …). */} + {renderSlot('settings.general.item', {})} +
+ ) +} diff --git a/packages/client/ui-settings/src/client/contract/slots.ts b/packages/client/ui-settings/src/client/contract/slots.ts index 10e4e0ecbe..87db2f8999 100644 --- a/packages/client/ui-settings/src/client/contract/slots.ts +++ b/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 diff --git a/packages/client/ui-settings/src/client/index.ts b/packages/client/ui-settings/src/client/index.ts index 3613476ede..c360a84201 100644 --- a/packages/client/ui-settings/src/client/index.ts +++ b/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') } diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings/src/client/locales.ts similarity index 64% rename from packages/client/ui-settings-general/src/client/locales.ts rename to packages/client/ui-settings/src/client/locales.ts index ff10ce5c37..6fc3295561 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/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', } diff --git a/packages/client/ui-settings/tests/apply.spec.ts b/packages/client/ui-settings/tests/apply.spec.ts index a94406e4ce..95684404a1 100644 --- a/packages/client/ui-settings/tests/apply.spec.ts +++ b/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() + }) +}) diff --git a/packages/client/ui-settings/tests/general-section.spec.tsx b/packages/client/ui-settings/tests/general-section.spec.tsx new file mode 100644 index 0000000000..a944dd8b15 --- /dev/null +++ b/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) =>
) as GeneralSectionComponentProps['renderSlot'], + ) + const props: GeneralSectionComponentProps = { + t: (key) => en[key] ?? key, + renderSlot, + } + const view = render() + 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() + }) +}) diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index 4c6ad92cd4..e60289b52d 100644 --- a/packages/client/ui-theme/package.json +++ b/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" } } diff --git a/packages/client/ui-theme/src/client/AppearanceRow.module.css b/packages/client/ui-theme/src/client/AppearanceRow.module.css new file mode 100644 index 0000000000..9619caa823 --- /dev/null +++ b/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); +} diff --git a/packages/client/ui-theme/src/client/AppearanceRow.tsx b/packages/client/ui-theme/src/client/AppearanceRow.tsx new file mode 100644 index 0000000000..b4aad1725d --- /dev/null +++ b/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> & 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 ( +
+
{t('appearance.title')}
+
+ {CUBES.map(({ id, labelKey, Icon }) => ( + + ))} +
+
+ ) +} diff --git a/packages/client/ui-theme/src/client/index.ts b/packages/client/ui-theme/src/client/index.ts index 5c7ca3fe12..ac79913062 100644 --- a/packages/client/ui-theme/src/client/index.ts +++ b/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 @@ -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 | undefined + const sync = (snapshot: ThemeSnapshot): void => { + bound?.sync(snapshot.preference, snapshot.revision) + } + ctx.on('theme/change', sync) + const injected = (actions: BoundActions): 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') } diff --git a/packages/client/ui-theme/src/client/settings-contract.ts b/packages/client/ui-theme/src/client/settings-contract.ts new file mode 100644 index 0000000000..4354ca8728 --- /dev/null +++ b/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 {} diff --git a/packages/client/ui-theme/src/client/settings-store.ts b/packages/client/ui-theme/src/client/settings-store.ts new file mode 100644 index 0000000000..256b04a299 --- /dev/null +++ b/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 { + 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 + }, + }, + }) +} diff --git a/packages/client/ui-theme/src/css-modules.d.ts b/packages/client/ui-theme/src/css-modules.d.ts new file mode 100644 index 0000000000..bc5e482353 --- /dev/null +++ b/packages/client/ui-theme/src/css-modules.d.ts @@ -0,0 +1,6 @@ +declare module '*.module.css' { + const classes: Record + export default classes +} + +declare module '*.css' diff --git a/packages/client/ui-theme/tests/invariant.spec.ts b/packages/client/ui-theme/tests/invariant.spec.ts index 6f4e867c4d..640599ea43 100644 --- a/packages/client/ui-theme/tests/invariant.spec.ts +++ b/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) }) diff --git a/packages/client/ui-theme/tsconfig.json b/packages/client/ui-theme/tsconfig.json index 51f9171643..7d5cc6f235 100644 --- a/packages/client/ui-theme/tsconfig.json +++ b/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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2410252eb0..40fee92c16 100644 --- a/pnpm-lock.yaml +++ b/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,70 +1022,6 @@ importers: specifier: ^18.2.0 version: 18.3.1 - packages/client/ui-settings-general: - 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-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 - '@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-settings-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-sidebar: dependencies: clsx: @@ -1087,13 +1066,35 @@ importers: version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) 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-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/ui-trajectory: devDependencies: diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index cb8ebb6a48..91554c5c5c 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -60,8 +60,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { '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.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 81e0fc989c..49a502571e 100644 --- a/tsconfig.base.json +++ b/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-*": [ diff --git a/tsconfig.client.json b/tsconfig.client.json index abe80b8b12..55ff2d9a8c 100644 --- a/tsconfig.client.json +++ b/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" }