refactor(gui): features register their own settings surfaces

Settings collaboration direction (recorded in the note): the shell only
provides composition faces — feature plugins register themselves. The
General section moves into the ui-settings shell (order 0, skeleton
rows) and declares the settings.general.item list slot; locale registers
the Language row and ui-theme the Appearance row (each with its own
store mirror, dictionaries, and ledger-judged deferral); the
ui-settings-general package is gone. ui-settings-models becomes
ui-models — a feature package that contributes its Settings section
rather than a settings-owned satellite. The item-slot SlotMap entry is
authored in the ui-settings contract and repeated verbatim in
locale/ui-theme (reference-cycle avoidance; declaration merging keeps
the copies identical).
This commit is contained in:
imccyu
2026-07-26 02:51:36 +08:00
parent 2ee4cda066
commit 23a60ade67
62 changed files with 1008 additions and 1049 deletions
@@ -0,0 +1,13 @@
/**
* Models settings section: an intentionally empty content column — the nav
* entry exists so the section slot composition is visible; model management
* lands in a later phase.
*/
/**
* Render the (empty) Models section content column.
* @returns null — no content this phase.
*/
export function ModelsSection() {
return null
}
@@ -0,0 +1,67 @@
/**
* Models settings section plugin, browser half. Registers the `models` nav
* entry into the shell-declared `settings.section` list slot; the content
* column is intentionally empty until model management lands. Export
* discipline: packages/client/AGENTS.md.
*/
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
// Type-only: pulls the shell's SlotMap merge (the 'settings.section' entry).
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
import type {} from '@deepseek-ai/dsh-client-locale/client'
import { ModelsSection } from './ModelsSection.tsx'
/**
* 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']
/**
* Register the Models section once the `settings.section` declaration is on
* the ledger.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.effect(() => {
const disposers = [
ctx.locale.register('settings.models', 'zh', { nav: '模型' }),
ctx.locale.register('settings.models', 'en', { nav: 'Models' }),
]
return () => { for (const dispose of disposers) dispose() }
}, '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
// the re-registration.
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 === ModelsSection)) return
dispose = ctx.slots.register({
name: 'settings.section',
id: 'models',
order: 10,
label: ctx.locale.bind('settings.models')('nav'),
}, ModelsSection)
}
// Nav labels are registrant-localized: re-register on locale change so
// the ledger carries fresh text (the version bump re-renders the shell).
// Dispose-then-requery: after an HMR collapse the disposer is stale and
// the ledger/spec re-check keeps this path an idempotent no-op.
const offLocale = ctx.on('locale/change', () => {
dispose?.()
dispose = undefined
tryRegister()
})
const unsubscribe = ctx.slots.subscribe('settings.section', () => { tryRegister() })
tryRegister()
return () => {
offLocale()
unsubscribe()
dispose?.()
}
}, 'ui-models: settings section registration')
}
+6
View File
@@ -0,0 +1,6 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.css'
+4
View File
@@ -0,0 +1,4 @@
/** Host loader entry for the browser implementation exported from `./client`. */
/** Host plugin body — no host-side behavior for the models settings plugin. */
export function apply(): void {}
@@ -0,0 +1,31 @@
/**
* 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-models'
/** Cordis companion plugin name. */
export const name = 'client-ui-models-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: a nav-entry-only section plugin rendering a fixed
* empty content column — it emits no cordis events and owns no cross-plugin
* mutable relation.
*/
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 */