diff --git a/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml new file mode 100644 index 0000000000..54fb6ee9ee --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write +2026-07-25-client-settings-locale-theme.md: 25e22c728ea509613c4d7f6cdfcd09faf4685760 +2026-07-25-client-settings-locale-theme.zh.md: 9c2457b254aa826291d4c5a4e115e9ba2d391974 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 new file mode 100644 index 0000000000..25e22c728e --- /dev/null +++ b/.agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md @@ -0,0 +1,112 @@ +# 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 + +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`. + +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. + +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 + +| section | 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) | + +The first phase localizes only the copy inside the Settings overlay (the General rows plus the navigation); copy on other pages is untouched. + +### Slot topology + +```text +root +└─ sidebar + └─ sidebar.settings single/root + └─ ui-settings + └─ settings.section list/root + ├─ general ui-settings-general + └─ models ui-settings-models +``` + +Section contributions use declaration-aware deferral and do not depend on the client manifest's apply order. + +### Service contracts + +```ts +type ThemePreference = 'light' | 'dark' | 'system' + +interface ThemeDefinition { + id: string + colorScheme: 'light' | 'dark' + tokens: Record +} + +interface ThemeSnapshot { + preference: ThemePreference + active: ThemeDefinition // system 已解析为具体 light/dark 定义 + themes: readonly ThemeDefinition[] + revision: number +} + +interface LocaleDefinition { + id: 'zh' | 'en' + label: string +} + +interface LocaleSnapshot { + active: 'zh' | 'en' + locales: readonly LocaleDefinition[] + revision: number +} + +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. + +**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. +- 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. +- 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 new section must keep declaration-aware registration and idempotence guards. Service events may fire before a section's first render, so both the General store's init and the controller attach must align to the current snapshot from the getters. 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. 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 d3c61794b9..9c2457b254 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 @@ -2,6 +2,8 @@ Status: proposed +[English](2026-07-25-client-settings-locale-theme.md) | 中文 + ## Problem 浏览器端已有的 Settings 直接写在 Sidebar 内,语言和主题也由组件本地状态直接改 DOM。这使 Settings 无法由独立插件扩展,偏好状态没有稳定的跨插件服务契约,主题 registry 同时承担状态与呈现职责。 @@ -49,6 +51,12 @@ section contribution 使用 declaration-aware deferral,不依赖 client manife ```ts type ThemePreference = 'light' | 'dark' | 'system' +interface ThemeDefinition { + id: string + colorScheme: 'light' | 'dark' + tokens: Record +} + interface ThemeSnapshot { preference: ThemePreference active: ThemeDefinition // system 已解析为具体 light/dark 定义 @@ -56,6 +64,11 @@ interface ThemeSnapshot { revision: number } +interface LocaleDefinition { + id: 'zh' | 'en' + label: string +} + interface LocaleSnapshot { active: 'zh' | 'en' locales: readonly LocaleDefinition[] diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 6c7627aca4..ede904e1a5 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2017,12 +2017,15 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts)) - `@deepseek-ai/dsh-client-connection` — requires `httpServer` · `apiProxy` ([`packages/client/connection/src/index.ts`](../packages/client/connection/src/index.ts)) -- `@deepseek-ai/dsh-client-i18n` ([`packages/client/i18n/src/index.ts`](../packages/client/i18n/src/index.ts)) +- `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts)) - `@deepseek-ai/dsh-client-modules` — requires `httpServer` · `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts)) - `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) - `@deepseek-ai/dsh-client-ui-layout` ([`packages/client/ui-layout/src/index.ts`](../packages/client/ui-layout/src/index.ts)) - `@deepseek-ai/dsh-client-ui-question` — requires `tools` · `userInteraction` ([`packages/client/ui-question/src/index.ts`](../packages/client/ui-question/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-settings-models` ([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts)) - `@deepseek-ai/dsh-client-ui-sidebar` ([`packages/client/ui-sidebar/src/index.ts`](../packages/client/ui-sidebar/src/index.ts)) - `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts)) - `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts)) diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d9521f7d67..2c1e85fb3c 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -63,6 +63,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `internal/dispatch` | - | [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `hmr`, `modules`, `webserver` | | `internal/status` | - | [`agent`](../packages/core/agent) | +| `locale/change` | `locale` (`emit`) | `ui-settings-general`, `ui-settings-models` | | `slots/changed` | `runtime` (`emit`) | - | +| `theme/change` | `ui-theme` (`emit`) | `ui-layout`, `ui-settings-general` | Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program. diff --git a/docs/module-graph.md b/docs/module-graph.md index 9e8b3adf8a..c47bd92d83 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -136,13 +136,16 @@ flowchart TD subgraph group_client["packages/client"] pkg_client_connection["client-connection"] pkg_client_hmr["client-hmr"] - pkg_client_i18n["client-i18n"] + pkg_client_locale["client-locale"] pkg_client_modules["client-modules"] pkg_client_runtime["client-runtime"] pkg_client_ui_conversation["client-ui-conversation"] pkg_client_ui_layout["client-ui-layout"] pkg_client_ui_primitives["client-ui-primitives"] pkg_client_ui_question["client-ui-question"] + pkg_client_ui_settings["client-ui-settings"] + pkg_client_ui_settings_general["client-ui-settings-general"] + pkg_client_ui_settings_models["client-ui-settings-models"] pkg_client_ui_sidebar["client-ui-sidebar"] pkg_client_ui_slots["client-ui-slots"] pkg_client_ui_theme["client-ui-theme"] @@ -225,7 +228,7 @@ flowchart TD pkg_subagent_subprocess --> pkg_invariants pkg_acp_snapshot --> pkg_invariants pkg_loader_smoke --> pkg_invariants - pkg_client_i18n --> pkg_invariants + pkg_client_locale --> pkg_invariants pkg_client_modules --> pkg_invariants pkg_client_runtime --> pkg_invariants pkg_client_ui_primitives --> pkg_invariants @@ -253,7 +256,19 @@ flowchart TD pkg_client_ui_conversation --> pkg_invariants pkg_client_ui_layout --> pkg_client_runtime pkg_client_ui_layout --> pkg_client_ui_slots + pkg_client_ui_layout --> pkg_client_ui_theme pkg_client_ui_layout --> pkg_invariants + pkg_client_ui_settings --> pkg_client_runtime + pkg_client_ui_settings --> pkg_client_ui_primitives + pkg_client_ui_settings --> pkg_client_ui_slots + pkg_client_ui_settings --> pkg_invariants + pkg_client_ui_settings_general --> pkg_client_runtime + pkg_client_ui_settings_general --> pkg_client_ui_primitives + pkg_client_ui_settings_general --> pkg_client_ui_slots + pkg_client_ui_settings_general --> pkg_invariants + pkg_client_ui_settings_models --> pkg_client_runtime + pkg_client_ui_settings_models --> pkg_client_ui_slots + pkg_client_ui_settings_models --> pkg_invariants pkg_client_ui_sidebar --> pkg_client_runtime pkg_client_ui_sidebar --> pkg_client_ui_primitives pkg_client_ui_sidebar --> pkg_client_ui_slots @@ -798,7 +813,7 @@ flowchart TD | [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | [`invariants`](../packages/support/invariants) | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants) | | [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`invariants`](../packages/support/invariants) | -| [`client-i18n`](../packages/client/i18n) | `client` | [`invariants`](../packages/support/invariants) | +| [`client-locale`](../packages/client/locale) | `client` | [`invariants`](../packages/support/invariants) | | [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/support/invariants) | | [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants) | | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/support/invariants) | @@ -817,7 +832,10 @@ flowchart TD | [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 0e59ce2116..f8f8a73304 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -79,7 +79,14 @@ export function apply(ctx: ClientContext): void { ctx.effect(() => { let dispose: (() => void) | undefined - const register = (): void => { + // 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', @@ -89,16 +96,13 @@ export function apply(ctx: ClientContext): void { inject: injected, }, GeneralSection) } - const tryRegister = (): void => { - if (ctx.slots.spec('settings.section') === undefined || dispose !== undefined) return - register() - } // Nav labels are registrant-localized: re-register on locale change so // the ledger carries fresh text (the version bump re-renders the shell). const offLocale = ctx.on('locale/change', () => { - if (dispose === undefined) return - dispose() - register() + if (!registered()) return + dispose?.() + dispose = undefined + tryRegister() }) const unsubscribe = ctx.slots.subscribe('settings.section', () => { tryRegister() }) tryRegister() diff --git a/packages/client/ui-theme/src/client/index.ts b/packages/client/ui-theme/src/client/index.ts index 5ba5d40b35..b8a75c4bed 100644 --- a/packages/client/ui-theme/src/client/index.ts +++ b/packages/client/ui-theme/src/client/index.ts @@ -157,6 +157,7 @@ export class ThemeService { : this.preference // Both built-ins always exist; a registered preference id resolves or has // been reset by its disposer, so the lookup cannot miss. + /* v8 ignore next -- the ?? arm needs a registry without light/dark, which register()/dispose() cannot produce */ const active = this.themes.find(t => t.id === resolvedId) ?? this.themes[0]! return Object.freeze({ preference: this.preference, diff --git a/packages/client/ui-theme/tests/theme.spec.ts b/packages/client/ui-theme/tests/theme.spec.ts index 9ab0ab8f0f..fdac111e73 100644 --- a/packages/client/ui-theme/tests/theme.spec.ts +++ b/packages/client/ui-theme/tests/theme.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client' import { STORAGE_KEY, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client' @@ -87,4 +87,53 @@ describe('ThemeService', () => { dispose() expect(events.map(e => e.revision)).toEqual([1, 2, 3, 4]) }) + + describe('prefers-color-scheme resolution (stubbed matchMedia)', () => { + type Listener = () => void + const stubMedia = (initialMatches: boolean) => { + const listeners = new Set() + const media = { + matches: initialMatches, + addEventListener: (_: 'change', fn: Listener) => { listeners.add(fn) }, + removeEventListener: (_: 'change', fn: Listener) => { listeners.delete(fn) }, + flip() { + this.matches = !this.matches + for (const fn of listeners) fn() + }, + listenerCount: () => listeners.size, + } + vi.stubGlobal('matchMedia', () => media) + return media + } + + afterEach(() => { vi.unstubAllGlobals() }) + + it('system resolves against the media query and follows OS flips', () => { + const media = stubMedia(true) + const { theme, events } = make() + expect(theme.getTheme().preference).toBe('system') + expect(theme.getTheme().active.id).toBe('dark') + media.flip() + expect(theme.getTheme().active.id).toBe('light') + expect(events).toHaveLength(1) + }) + + it('OS flips do not republish while a concrete preference is set', () => { + const media = stubMedia(false) + const { theme, events } = make() + theme.setTheme('light') + expect(events).toHaveLength(1) + media.flip() + expect(events).toHaveLength(1) + expect(theme.getTheme().active.id).toBe('light') + }) + + it('context dispose releases the media listener', async () => { + const media = stubMedia(false) + const { ctx } = make() + expect(media.listenerCount()).toBe(1) + await ctx.fiber.dispose() + expect(media.listenerCount()).toBe(0) + }) + }) }) diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index da5fe34dc4..cb8ebb6a48 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -59,6 +59,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' }, '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/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.' },