refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -2,5 +2,5 @@
# 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 packages/client/ui-theme/README.md
README.md: df4d5e0370962bf6f2a8ac0a7b88d669225dc5c5
README.zh.md: fb8e937979a5168803be78a49e1bc1e7eacd7a47
README.md: 9388d5c05e5625d1a38b27c4565edf3ed47b3b6d
README.zh.md: 9995980e4a64b07c376f90e223c1ec83b5881b73

View File

@@ -2,9 +2,9 @@
English | [中文](README.zh.md)
Theme plugin: ThemeService over the --dsw-* token base stylesheets (static scale + alias semantic layers). The service owns the live theme preference (`light`/`dark`/`system`), resolves `system` through `prefers-color-scheme`, and publishes immutable `ThemeSnapshot`s on the `theme/change` event; it never touches the DOM — ui-layout's presenter applies the resolved snapshot (`html { color-scheme }`, `body[data-ds-dark-theme]`, and inline alias tokens). A loopback browser provides the service immediately with `system`, then loads `ui-theme.preference` in the background and writes each built-in selection through the Host settings API, whose local provider stores it in `$DSH_HOME/settings.yaml` by default; pushed settings changes and reconnects refetch it, rapid selections are serialized in gesture order with namespace revisions, and a rejected latest write reloads the durable value. A remote browser cannot access the privileged settings API, so its selection remains process-local. Third-party registered theme ids remain an in-process extension and do not cross the built-in settings schema; removing one never overwrites the last durable built-in preference. The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary.
Theme plugin: ThemeRuntime over the --dsw-* token base stylesheets (static scale + alias semantic layers). The service owns the live theme preference (`light`/`dark`/`system`), resolves `system` through `prefers-color-scheme`, and publishes immutable `ThemeSnapshot`s on the `theme/change` event; it never touches the DOM — ui-layout's presenter applies the resolved snapshot (`html { color-scheme }`, `body[data-ds-dark-theme]`, and inline alias tokens). A loopback browser provides the service immediately with `system`, then loads `ui-theme.preference` in the background and writes each built-in selection through the Host settings API, whose local provider stores it in `$DSH_HOME/settings.yaml` by default; pushed settings changes and reconnects refetch it, rapid selections are serialized in gesture order with namespace revisions, and a rejected latest write reloads the durable value. A remote browser cannot access the privileged settings API, so its selection remains process-local. Third-party registered theme ids remain an in-process extension and do not cross the built-in settings schema; removing one never overwrites the last durable built-in preference. The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary.
When the host composition includes an HTTP server, the host half injects a synchronous bootstrap immediately after the opening `<body>` tag. Each index response embeds the registered Host setting for `ui-theme.preference`, or `system` when no settings provider is present; the browser resolves `system` from the OS scheme, then sets `color-scheme` and `body[data-ds-dark-theme]` before the shell loading page renders. Compositions without an HTTP server remain unaffected, and ThemeService and ui-layout remain authoritative for client state and subsequent DOM updates after the plugin tree activates.
When the host composition includes an HTTP server, the host half injects a synchronous bootstrap immediately after the opening `<body>` tag. Each index response embeds the registered Host setting for `ui-theme.preference`, or `system` when no settings provider is present; the browser resolves `system` from the OS scheme, then sets `color-scheme` and `body[data-ds-dark-theme]` before the shell loading page renders. Compositions without an HTTP server remain unaffected, and ThemeRuntime and ui-layout remain authoritative for client state and subsequent DOM updates after the plugin tree activates.
`src/styles/` holds five sheets, all imported by the web shell's `base.css`: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.

View File

@@ -2,9 +2,9 @@
[English](README.md) | 中文
主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的 ThemeService。该服务拥有实时主题偏好`light``dark``system`),将 `system` 通过 `prefers-color-scheme` 解析为实际主题,并发布不可变的 `ThemeSnapshot`,通过 `theme/change` 事件通知变化;它绝不接触 DOMui-layout 的呈现器会应用解析后的快照(`html { color-scheme }``body[data-ds-dark-theme]`,以及主题的别名 token 内联变量)。来自回环地址的浏览器会先以 `system` 立即提供该服务,随后在后台加载 `ui-theme.preference`,并将每次内置主题选择通过 Host settings API 写入;其本地提供方默认将设置存入 `$DSH_HOME/settings.yaml`。收到推送的 settings 变更时或重连后,浏览器都会重新拉取该设置;连续快速选择会按操作顺序携带 namespace revision 串行写入,最新写入被拒时则重新加载持久化值。远程浏览器无法访问特权 settings API因此它的选择仅保留在进程内。已注册的第三方主题 id 仍是进程内扩展,不会跨越内置 settings schema移除其中任意一个都绝不会覆盖最后一个持久化的内置偏好。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md)拥有。
主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的 ThemeRuntime。该服务拥有实时主题偏好`light``dark``system`),将 `system` 通过 `prefers-color-scheme` 解析为实际主题,并发布不可变的 `ThemeSnapshot`,通过 `theme/change` 事件通知变化;它绝不接触 DOMui-layout 的呈现器会应用解析后的快照(`html { color-scheme }``body[data-ds-dark-theme]`,以及主题的别名 token 内联变量)。来自回环地址的浏览器会先以 `system` 立即提供该服务,随后在后台加载 `ui-theme.preference`,并将每次内置主题选择通过 Host settings API 写入;其本地提供方默认将设置存入 `$DSH_HOME/settings.yaml`。收到推送的 settings 变更时或重连后,浏览器都会重新拉取该设置;连续快速选择会按操作顺序携带 namespace revision 串行写入,最新写入被拒时则重新加载持久化值。远程浏览器无法访问特权 settings API因此它的选择仅保留在进程内。已注册的第三方主题 id 仍是进程内扩展,不会跨越内置 settings schema移除其中任意一个都绝不会覆盖最后一个持久化的内置偏好。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md)拥有。
当主机组合包含 HTTP 服务器时,主机侧紧接 `<body>` 起始标签注入同步引导代码。每份 index 响应会嵌入已注册的 Host 设置 `ui-theme.preference`,没有 settings provider 时则嵌入 `system`;浏览器按操作系统配色解析 `system`,随后在外壳加载页面渲染前设置 `color-scheme``body[data-ds-dark-theme]`。不含 HTTP 服务器的组合不受影响插件树激活后ThemeService 与 ui-layout 仍分别是客户端状态和后续 DOM 更新的权威来源。
当主机组合包含 HTTP 服务器时,主机侧紧接 `<body>` 起始标签注入同步引导代码。每份 index 响应会嵌入已注册的 Host 设置 `ui-theme.preference`,没有 settings provider 时则嵌入 `system`;浏览器按操作系统配色解析 `system`,随后在外壳加载页面渲染前设置 `color-scheme``body[data-ds-dark-theme]`。不含 HTTP 服务器的组合不受影响插件树激活后ThemeRuntime 与 ui-layout 仍分别是客户端状态和后续 DOM 更新的权威来源。
`src/styles/` 下有五张样式表,全部由 web 壳的 `base.css` 导入:`base.css``design-platform.css``scrollbar.css``gradient-shadow-text.css``shiki.css``scrollbar.css``--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-client-ui-theme",
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeService for light/dark/system state; --dsw-* token styles and Appearance settings row",
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"

View File

@@ -69,7 +69,7 @@ export interface ThemeSnapshot {
declare module '@deepseek-ai/cordis' {
interface Context {
theme: ThemeService
theme: ThemeRuntime
}
interface Events {
/**
@@ -96,7 +96,7 @@ const BUILTIN_THEMES: readonly ThemeDefinition[] = Object.freeze([
* sensing, not presentation) and re-emits when the OS scheme flips while the
* preference is `system`.
*/
export class ThemeService {
export class ThemeRuntime {
private readonly ctx: Context
private readonly host: SettingsScope<ThemeSettings>
private themes: ThemeDefinition[] = [...BUILTIN_THEMES]
@@ -228,7 +228,7 @@ export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope
*/
export function apply(ctx: ClientContext): void {
const host = ctx.settingsScope.bind<ThemeSettings>({ namespace: THEME_SETTINGS_NAMESPACE })
const theme = new ThemeService(ctx, host)
const theme = new ThemeRuntime(ctx, host)
ctx.provide('theme', theme)
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), 'ui-theme: settings row dictionaries')

View File

@@ -34,9 +34,9 @@ export function apply(ctx: Context): void {
ctx.inject(['settings'], (settingsCtx) => {
settingsCtx.settings.register(THEME_NAMESPACE, ThemeSettingsSchema)
})
ctx.inject(['httpServer'], (httpCtx) => {
ctx.inject(['webServer'], (httpCtx) => {
httpCtx.effect(
() => httpCtx.httpServer.tapIndex(html => injectBootTheme(html, readPreference(ctx))),
() => httpCtx.webServer.tapIndex(html => injectBootTheme(html, readPreference(ctx))),
'client-ui-theme: initial theme bootstrap',
)
})

View File

@@ -22,7 +22,7 @@ const COPY: Record<string, string> = {
/** Empty global standard-kit hooks (the row reads neither). */
function emptySessions() {
const store = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined })
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, jobsBySession: {}, currentAddress: undefined })
return bindSnapshotSelector(store)
}
function emptyWorkspaces() {

View File

@@ -3,12 +3,12 @@
* projection into the row store, and HMR collapse recovery. */
import { Context } from '@deepseek-ai/cordis'
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 { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client'
import { SettingsScopeBinder } from '@deepseek-ai/dsh-client-ui-settings/client'
import { apply, inject, SETTINGS_NS } from '@deepseek-ai/dsh-client-ui-theme/client'
import type { AppearanceRowInjected, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client'
import type { AppearanceRowInjected, ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'
import { THEME_SETTINGS_NAMESPACE, ThemeSettingsSchema } from '../src/theme-settings.ts'
import { AppearanceRow } from '../src/client/AppearanceRow.tsx'
import type { createAppearanceRowStore } from '../src/client/settings-store.ts'
@@ -27,8 +27,8 @@ function deferred<T>() {
async function bench(isLoopback = true) {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
await ctx.plugin(SlotRegistry).await()
const locale = new LocaleRuntime(ctx)
ctx.provide('locale', locale)
let preference = 'system'
const namespace = () => ({
@@ -56,15 +56,15 @@ async function bench(isLoopback = true) {
ctx.provide('connection', { api: { settings: { describe, mutate } }, isLoopback } as never)
// The settings transport and the forwarded-event port the plugin injects.
new TestRemote(ctx)
await ctx.plugin(SettingsScopeService).await()
await ctx.plugin(SettingsScopeBinder).await()
return {
ctx, slots: ctx.get('slots') as SlotsService, locale, describe, mutate,
ctx, slots: ctx.get('slots') as SlotRegistry, locale, describe, mutate,
setHostPreference: (next: string) => { preference = next },
}
}
/** Stand in for the settings shell: declare the General item slot from root. */
function declareItems(slots: SlotsService): () => void {
function declareItems(slots: SlotRegistry): () => void {
return slots.register(
{ name: 'root', children: { [SLOT]: { kind: 'list', scope: 'root' } } } as never,
() => null,
@@ -73,7 +73,7 @@ function declareItems(slots: SlotsService): () => void {
/** 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) {
function faceOf(slots: SlotRegistry) {
const entry = slots.entries(SLOT).find(e => e.component === AppearanceRow)!
const handle = entry.store as ReturnType<typeof createAppearanceRowStore>
const instance = handle.create()
@@ -109,7 +109,7 @@ describe('ui-theme apply', () => {
const b = await bench()
declareItems(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const theme = b.ctx.get('theme') as ThemeService
const theme = b.ctx.get('theme') as ThemeRuntime
// An event ahead of any inject hits the unbound-actions arm.
theme.setTheme('dark')
@@ -130,7 +130,7 @@ describe('ui-theme apply', () => {
b.setHostPreference('dark')
declareItems(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const theme = b.ctx.get('theme') as ThemeService
const theme = b.ctx.get('theme') as ThemeRuntime
await vi.waitFor(() => { expect(theme.getTheme().preference).toBe('dark') })
b.ctx.remote.$dispatch('settings/document-updated', ['unrelated', 0])
expect(b.describe).toHaveBeenCalledOnce()
@@ -144,7 +144,7 @@ describe('ui-theme apply', () => {
const remote = await bench(false)
declareItems(remote.slots)
await remote.ctx.plugin({ inject: [...inject], apply }).await()
const remoteTheme = remote.ctx.get('theme') as ThemeService
const remoteTheme = remote.ctx.get('theme') as ThemeRuntime
remoteTheme.setTheme('dark')
await Promise.resolve()
expect(remote.describe).not.toHaveBeenCalled()
@@ -159,7 +159,7 @@ describe('ui-theme apply', () => {
b.describe.mockImplementationOnce(() => pending.promise)
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
const theme = b.ctx.get('theme') as ThemeService
const theme = b.ctx.get('theme') as ThemeRuntime
expect(theme.getTheme().preference).toBe('system')
pending.resolve(await describe())
await vi.waitFor(() => { expect(theme.getTheme().preference).toBe('dark') })
@@ -170,7 +170,7 @@ describe('ui-theme apply', () => {
const b = await bench()
b.setHostPreference('sepia')
await b.ctx.plugin({ inject: [...inject], apply }).await()
const theme = b.ctx.get('theme') as ThemeService
const theme = b.ctx.get('theme') as ThemeRuntime
await vi.waitFor(() => { expect(b.describe).toHaveBeenCalledOnce() })
expect(theme.getTheme().preference).toBe('system')
})

View File

@@ -1,12 +1,12 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import type { HttpServerService } from '@deepseek-ai/dsh-host-webserver'
import { Settings, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import type { WebServer } from '@deepseek-ai/dsh-host-webserver'
import { SettingsProvider, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
import {
DEFAULT_PREFERENCE, THEME_SETTINGS_NAMESPACE, apply,
} from '@deepseek-ai/dsh-client-ui-theme'
class MemorySettings extends Settings {
class MemorySettings extends SettingsProvider {
readonly writable = true
protected load(): Promise<Record<string, unknown>> { return Promise.resolve({}) }
protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> {
@@ -34,12 +34,12 @@ describe('ui-theme host', () => {
await ctx.plugin(MemorySettings).await()
let transform: ((html: string) => string) | undefined
let disposed = false
ctx.provide('httpServer', {
ctx.provide('webServer', {
tapIndex: (next: (html: string) => string) => {
transform = next
return () => { disposed = true }
},
} as HttpServerService)
} as WebServer)
const fiber = ctx.plugin({ apply })
await fiber.await()
expect(transform?.('<body></body>')).toContain('const preference = "system"')
@@ -53,12 +53,12 @@ describe('ui-theme host', () => {
it('uses the system preference when only an HTTP server exists', async () => {
const ctx = new Context()
let transform: ((html: string) => string) | undefined
ctx.provide('httpServer', {
ctx.provide('webServer', {
tapIndex: (next: (html: string) => string) => {
transform = next
return () => undefined
},
} as HttpServerService)
} as WebServer)
await ctx.plugin({ apply }).await()
expect(transform?.('<body></body>')).toContain('const preference = "system"')
})

View File

@@ -2,17 +2,17 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/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 { apply as clientApply, inject, ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'
import * as ThemeInvariant from '@deepseek-ai/dsh-client-ui-theme/invariant'
import { apply as localeApply, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import InvariantService from '@deepseek-ai/dsh-invariants'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
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 ctx.plugin(InvariantRegistry, { enabled: true })
await expect(ctx.plugin(ThemeInvariant).await()).resolves.toBeDefined()
})
@@ -26,7 +26,7 @@ describe('invariant companion', () => {
// copy, hence the slots + locale edges.
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope'])
const ctx = new Context()
new SlotsService(ctx)
new SlotRegistry(ctx)
ctx.provide('connection', {
api: { settings: { describe: () => Promise.resolve({
rpcId: 'theme-invariant' as never,
@@ -39,6 +39,6 @@ describe('invariant companion', () => {
ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never)
await ctx.plugin({ inject: localeInject, apply: localeApply }).await()
await ctx.plugin({ inject, apply: clientApply }).await()
expect(ctx.get('theme')).toBeInstanceOf(ThemeService)
expect(ctx.get('theme')).toBeInstanceOf(ThemeRuntime)
})
})

View File

@@ -3,21 +3,21 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { stubSettingsScope, type StubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime'
import type { ThemeSettings, ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
import { ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client'
import { ThemeRuntime } from '@deepseek-ai/dsh-client-ui-theme/client'
const make = (host = stubSettingsScope<ThemeSettings>()): {
ctx: Context
theme: ThemeService
theme: ThemeRuntime
events: ThemeSnapshot[]
host: StubSettingsScope<ThemeSettings>
} => {
const ctx = new Context()
const events: ThemeSnapshot[] = []
ctx.on('theme/change', (snapshot) => { events.push(snapshot) })
return { ctx, theme: new ThemeService(ctx, host.scope), events, host }
return { ctx, theme: new ThemeRuntime(ctx, host.scope), events, host }
}
describe('ThemeService', () => {
describe('ThemeRuntime', () => {
it('defaults to the system preference resolved against prefers-color-scheme', () => {
const { theme } = make()
const snapshot = theme.getTheme()

View File

@@ -30,7 +30,7 @@
"path": "../../settings/settings"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../ui-settings"