mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into worktree/i18n-complete-non-readme
# Conflicts: # packages/client/i18n/README.md # packages/client/locale/README.i18n.yaml # packages/client/locale/README.zh.md # packages/client/ui-layout/README.md # packages/client/ui-theme/README.md
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-25-client-settings-locale-theme.md: 87077b3fd3f0bd8a3375a71aebf947cbd9961799
|
||||
2026-07-25-client-settings-locale-theme.zh.md: a64a4afdf6565a527a25136694aa79305eeabb3c
|
||||
@@ -0,0 +1,130 @@
|
||||
# Agent Note: Client Settings, Locale, and Theme layering
|
||||
|
||||
Status: proposed
|
||||
|
||||
English | [中文](2026-07-25-client-settings-locale-theme.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The browser client's existing Settings is written directly inside the Sidebar, and language and theme are applied by component-local state mutating the DOM directly. As a result Settings cannot be extended by independent plugins, preference state has no stable cross-plugin service contract, and the theme registry carries both state and presentation responsibilities.
|
||||
|
||||
## Proposal
|
||||
|
||||
**Collaboration doctrine (how every later module joins Settings): feature owners self-register.** The Settings shell is a pure composition surface: it only declares slots and renders the chrome structure — zero copy, no locale dependency, and neither importing nor enumerating 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). Content that belongs to no single feature (the trigger/title/close chrome copy, the General directory with its skeleton rows, the `settings` dictionary) is owned by `ui-settings-general` — the owner of the ownerless copy, not a feature satellite package.
|
||||
|
||||
The Sidebar declares the `sidebar.settings` single slot; `ui-settings` occupies it and declares four slots: `settings.trigger` / `settings.header` / `settings.close` (chrome content seats, single) and `settings.section` (top-level pages, list). Accessible names all resolve from slot content: the trigger's accessible name is its text content, the dialog points at the header content node via aria-labelledby, and close is a visually hidden text seat. Each section is contributed by a feature plugin; the shell only reads entry metadata from the slot ledger to build the navigation, rendering the current section via `only`. General is registered by `ui-settings-general` (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).
|
||||
|
||||
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 registration surfaces
|
||||
|
||||
| Registration surface | Owning plugin | First-phase content |
|
||||
|---|---|---|
|
||||
| chrome content (trigger/header/close) | `ui-settings-general` | Settings entry-row icon and copy, panel title, close hidden text |
|
||||
| General section (order 0) | `ui-settings-general` | 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; dictionaries stay close to their owners — the chrome plus the General skeletons live in `ui-settings-general`'s `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.trigger single/root ui-settings-general 注册
|
||||
├─ settings.header single/root ui-settings-general 注册
|
||||
├─ settings.close single/root ui-settings-general 注册
|
||||
└─ settings.section list/root
|
||||
├─ general (order 0) ui-settings-general 注册
|
||||
│ └─ settings.general.item list/root
|
||||
│ ├─ language (0) locale 注册
|
||||
│ └─ appearance (10) ui-theme 注册
|
||||
└─ models (order 10) ui-models 注册
|
||||
```
|
||||
|
||||
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, `refresh()` for localized labels, one-call disposal) and do not depend on the client manifest's apply order. The SlotMap types split homes: trigger/header/close/section have their canonical home in the ui-settings contract (the consumers, general and models, both depend on the shell — no cycle); `settings.general.item`'s canonical home is the locale package — it is the lowest common dependency of all item registrants (a settings row always carries copy), while the declarer general's contract is unreachable from locale/ui-theme (it would form a cycle); ui-theme consumes it through a re-export seam.
|
||||
|
||||
### Future work: promote slot declarations to first-class injectable waits
|
||||
|
||||
`deferRegistration()` is behaviorally isomorphic to `ctx.inject` — one waits on a ledger declaration, the other on service presence, with matching disappear/reappear lifecycle semantics; the difference is that the fiber form's disposer lifetime naturally equals the declaration's lifetime, so the stale-disposer presence-judging machinery disappears entirely. Direction (a separate PR): SlotsService bridges each slot into a `slot:<name>` service (value = the slot spec) at declaration commit / cascade removal, registrants migrate from `deferRegistration()` to a nested `ctx.inject(['slot:<name>'], cb)`, then `deferRegistration()` is deleted and packages/client/AGENTS.md checklist item 4 is rewritten. Boundaries to pin down: the nested fiber's harmless wait must not be named by the boot fail-loud scan (needs a test); the `slot:` namespace and the silent-wait-on-typo stance; provide keys are flat names (`slot:a.b` is one key, not a property path on `ctx.slots`). This phase keeps the `deferRegistration()` function form.
|
||||
|
||||
### Service contracts
|
||||
|
||||
```ts
|
||||
export type ThemePreference = 'light' | 'dark' | 'system'
|
||||
|
||||
export interface ThemeDefinition {
|
||||
id: string
|
||||
colorScheme: 'light' | 'dark'
|
||||
tokens: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ThemeSnapshot {
|
||||
preference: ThemePreference
|
||||
active: ThemeDefinition // system 已解析为具体 light/dark 定义
|
||||
themes: readonly ThemeDefinition[]
|
||||
revision: number
|
||||
}
|
||||
|
||||
export interface LocaleDefinition {
|
||||
id: 'zh' | 'en'
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface LocaleSnapshot {
|
||||
active: 'zh' | 'en'
|
||||
locales: readonly LocaleDefinition[]
|
||||
revision: number
|
||||
}
|
||||
|
||||
export interface Events {
|
||||
/** @param snapshot - Current locale registry snapshot. @mode emit */
|
||||
'locale/change'(snapshot: LocaleSnapshot): void
|
||||
/** @param snapshot - Current theme registry snapshot. @mode emit */
|
||||
'theme/change'(snapshot: ThemeSnapshot): void
|
||||
}
|
||||
```
|
||||
|
||||
Locale ships with 中文 and English built in; `setLocale`/`setTheme` are the only write entry points, and an unknown id fails.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Having the app shell subscribe to preferences centrally and re-render the root slot tree.** A language or theme change only needs to update the actual consumers; a whole-tree refresh amplifies the blast radius and wires business preferences into the shell.
|
||||
|
||||
**The theme service mutating the DOM directly.** The registry service would then depend on the presentation environment, with unclear lifecycle and global-style ownership; Layout already owns the page-root presentation boundary.
|
||||
|
||||
**Resolving system in the Layout presenter.** The presenter would need its own matchMedia subscription and would pick the concrete definition out of the themes list, forcing the presentation layer to understand preference semantics; resolving on the service side gives every consumer the same resolved snapshot.
|
||||
|
||||
**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.
|
||||
|
||||
**A per-feature `ui-settings-*` satellite package for each section.** It divorces the settings surface from the feature itself: changing Theme behavior touches two packages, the package count grows linearly with settings items, and the satellite packages depending back on the locale/theme services form an intermediate layer that exists purely for the package split. Under feature-owner self-registration that layer does not exist: preference rows ship with their feature packages, and `ui-settings-general` takes in only the ownerless copy (the chrome and the General skeletons), carrying no feature's settings surface.
|
||||
|
||||
**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 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.
|
||||
- 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.
|
||||
- The overlay closes via the close button, a mask click, and ESC.
|
||||
|
||||
## Risks
|
||||
|
||||
The apply order of slot declarations and contributions is not fixed, so every section/item registrant must keep declaration-aware registration and judge presence by the ledger, not by a local disposer. Service events may fire before a row's first render, so both a feature row store's init and the inject attach must align to the current snapshot from the getter. The duplicated merge copies of `settings.general.item` (locale, ui-theme) must stay verbatim-identical to the ui-settings canonical home — any drift means changing all three together. Layout must clean up the global attributes it set on unmount, and ThemeService must remove its matchMedia listener on dispose, so nothing lingers after HMR.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Agent Note: Client Settings、Locale 与 Theme 分层
|
||||
|
||||
Status: proposed
|
||||
|
||||
[English](2026-07-25-client-settings-locale-theme.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
浏览器端已有的 Settings 直接写在 Sidebar 内,语言和主题也由组件本地状态直接改 DOM。这使 Settings 无法由独立插件扩展,偏好状态没有稳定的跨插件服务契约,主题 registry 同时承担状态与呈现职责。
|
||||
|
||||
## Proposal
|
||||
|
||||
**协作导向(后续所有模块接入 Settings 的方式):功能属主自注册。** Settings 壳是纯组合面:只声明坑位、渲染 chrome 结构,零文案、不依赖 locale、不 import 也不枚举任何功能;一个功能要出现在 Settings 里,由它自己的插件向对应坑位注册——locale 注册 Language 行,ui-theme 注册 Appearance 行,ui-models 注册 Models 一级面板。不为「某功能的设置页」单开 `ui-settings-*` 包:设置面属于功能包本身(做 Theme 功能,Theme 的设置选择就随 ui-theme 一起交付)。不属于任何单一功能的内容(trigger/标题/close 的 chrome 文案、General 目录与骨架行、`settings` 字典)由 `ui-settings-general` 拥有——它是「无主文案」的属主,不是功能卫星包。
|
||||
|
||||
Sidebar 声明 `sidebar.settings` 单坑位,`ui-settings` 占用它并声明四个坑:`settings.trigger` / `settings.header` / `settings.close`(chrome 内容座,single)与 `settings.section`(一级页面,list)。无障碍名全部解析自坑内容:trigger 的可达名即其文本内容,dialog 经 aria-labelledby 指向 header 内容节点,close 是视觉隐藏文本座。每个 section 由功能插件贡献;壳只从 slot ledger 读取 entry metadata 生成导航,通过 `only` 渲染当前 section。General 由 `ui-settings-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,坏值回退默认)。
|
||||
|
||||
功能行的 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,只消费已解析结果。
|
||||
|
||||
### 首期注册面
|
||||
|
||||
| 注册面 | 属主插件 | 首期内容 |
|
||||
|---|---|---|
|
||||
| chrome 内容(trigger/header/close)| `ui-settings-general` | 设置入口行图标+文案、面板标题、close 隐藏文本 |
|
||||
| General section(order 0)| `ui-settings-general` | 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 浮层内文案;字典就近——chrome + General 骨架归 `ui-settings-general` 的 `settings` namespace,功能行文案归各功能包(`settings.locale`、`settings.theme`、`settings.models`)。
|
||||
|
||||
### Slot topology
|
||||
|
||||
```text
|
||||
root
|
||||
└─ sidebar
|
||||
└─ sidebar.settings single/root
|
||||
└─ ui-settings(壳,零文案)
|
||||
├─ settings.trigger single/root ui-settings-general 注册
|
||||
├─ settings.header single/root ui-settings-general 注册
|
||||
├─ settings.close single/root ui-settings-general 注册
|
||||
└─ settings.section list/root
|
||||
├─ general (order 0) ui-settings-general 注册
|
||||
│ └─ settings.general.item list/root
|
||||
│ ├─ language (0) locale 注册
|
||||
│ └─ appearance (10) ui-theme 注册
|
||||
└─ models (order 10) ui-models 注册
|
||||
```
|
||||
|
||||
section/item contribution 均使用 declaration-aware deferral(ui-slots 的 `deferRegistration()`:ledger 判在位、`refresh()` 换本地化 label、一键 dispose),不依赖 client manifest 的 apply 顺序。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
|
||||
|
||||
### Future work:坑位声明升格为可 inject 的一等等待物
|
||||
|
||||
`deferRegistration()` 与 `ctx.inject` 行为同构——一个等 ledger 声明、一个等服务在场,消失/重现的生命周期语义一致;差别只在 fiber 版的 disposer 生命周期天然等于声明生命周期,stale-disposer 判在位机器可整体消失。方向(另开 PR):SlotsService 在声明落账/级联拆除处把每个坑位桥接成 `slot:<name>` 服务(value 为坑位 spec),注册方从 `deferRegistration()` 迁为嵌套 `ctx.inject(['slot:<name>'], cb)`,随后删除 `deferRegistration()` 并改写 packages/client/AGENTS.md checklist 第 4 条。待钉死的边界:嵌套 fiber 的无害等待不被 boot fail-loud 扫描点名(需测试);`slot:` 名字空间与 typo 静默等待的口径;provide 键是平面名(`slot:a.b` 是一个键,不是 `ctx.slots` 的属性路径)。本期维持 `deferRegistration()` 函数形式。
|
||||
|
||||
### Service contracts
|
||||
|
||||
```ts
|
||||
export type ThemePreference = 'light' | 'dark' | 'system'
|
||||
|
||||
export interface ThemeDefinition {
|
||||
id: string
|
||||
colorScheme: 'light' | 'dark'
|
||||
tokens: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ThemeSnapshot {
|
||||
preference: ThemePreference
|
||||
active: ThemeDefinition // system 已解析为具体 light/dark 定义
|
||||
themes: readonly ThemeDefinition[]
|
||||
revision: number
|
||||
}
|
||||
|
||||
export interface LocaleDefinition {
|
||||
id: 'zh' | 'en'
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface LocaleSnapshot {
|
||||
active: 'zh' | 'en'
|
||||
locales: readonly LocaleDefinition[]
|
||||
revision: number
|
||||
}
|
||||
|
||||
export interface Events {
|
||||
/** @param snapshot - Current locale registry snapshot. @mode emit */
|
||||
'locale/change'(snapshot: LocaleSnapshot): void
|
||||
/** @param snapshot - Current theme registry snapshot. @mode emit */
|
||||
'theme/change'(snapshot: ThemeSnapshot): void
|
||||
}
|
||||
```
|
||||
|
||||
Locale 内置中文和 English;`setLocale`/`setTheme` 是唯一写入口,未知 id 失败。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**由 app shell 统一订阅偏好并重渲染 root slot tree。** 语言和主题变化只需要更新实际消费者;全树刷新放大影响面,也把业务偏好接入 shell。
|
||||
|
||||
**Theme service 直接修改 DOM。** registry service 因此依赖呈现环境,生命周期与全局样式所有权不清;Layout 已经拥有页面根呈现边界。
|
||||
|
||||
**system 由 Layout presenter 解析。** presenter 需自带 matchMedia 订阅并在 themes 列表里挑选具体定义,呈现层被迫理解偏好语义;解析放服务侧则所有消费者拿到一致的已解析 snapshot。
|
||||
|
||||
**Settings import 并枚举各 section。** 新增页面必须修改壳插件,破坏「每个功能由自己的插件占坑」的组合模型。
|
||||
|
||||
**按功能为每个 section 单开 `ui-settings-*` 卫星包。** 设置面与功能本体分家:改 Theme 行为要动两个包,包数随设置项线性膨胀,且卫星包反向依赖 locale/theme 服务,形成纯粹为拆包而生的中间层。功能属主自注册下不存在这层:preference 行随功能包交付;`ui-settings-general` 只收无主文案(chrome 与 General 骨架),不承载任何功能的设置面。
|
||||
|
||||
**把 Locale/Theme snapshot 直接注入 React。** inject 结果按 entry identity 缓存,易变值会陈旧;为每个 service 自造 React hook 也绕开 slot store 的统一绑定。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Settings 壳只依赖 slot ledger,不依赖任一功能实现;General 的 item 列表同样只依赖 ledger。
|
||||
- 新增一个设置项 = 功能包自己注册(section 或 general item),零壳改动。
|
||||
- Locale 与 Theme 的写入只走 setter,持续同步只走 change event。
|
||||
- 功能行 store 初始化走 getter,后续由自家 change event 更新并局部重渲染。
|
||||
- Layout 独立应用 Theme snapshot,Theme service 不访问 DOM;presenter 不出现 system 分支。
|
||||
- 中文/English 与 Light/Dark/System 能切换并刷新后恢复;偏好为 system 时系统配色变化即时生效。
|
||||
- Models 只有导航项与空内容区;Permission、Tool Call 骨架无写操作。
|
||||
- 浮层经 close 按钮、遮罩点击、ESC 均可关闭。
|
||||
|
||||
## Risks
|
||||
|
||||
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 后残留。
|
||||
Reference in New Issue
Block a user