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:
@@ -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
|
||||
README.md: 94579cbdf955879edf2873e03cdd273d81c3b11e
|
||||
README.zh.md: e15390da27b94f10c6858417ed1faeabd45688ee
|
||||
README.md: 6aeda0e04bf690f5cc8a6d52eea95b1b2782a143
|
||||
README.zh.md: 9fbc2ed8392ae3ceba453517d9af5ef41f039d2d
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar is fixed-width (only details shrinks, then auto-closes); a closed sidebar retains a 56px control rail while details closes to zero width.
|
||||
Shell plugin: three-column AppFrame (drag handles and concession chain) plus the `ctx.layout` panel-geometry service; it registers into the runtime-owned `root` slot and declares `sidebar`, `conversation`, `details`, and `conversation.empty`. The sidebar is fixed-width (only details shrinks, then auto-closes); a closed sidebar retains a 56px control rail while details closes to zero width. The package also seats the theme presenter: it consumes resolved `ctx.theme` snapshots and projects them onto `document.body` (`data-ds-dark-theme` from the active color scheme plus the theme's alias tokens as inline variables).
|
||||
|
||||
AppFrame reads the runtime Session projection: `baselinesReady` selects loading, a page-local `SessionListState.intent` selects the empty composer, and a connected Session renders through `SessionProvider`. The conversation and empty-state owner shares are empty; each registrant obtains business data from standard hooks and actions from its own inject face. The sidebar owner share contains only `collapsed` and `width`; navigation actions belong to sidebar's own injected service face.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏宽度固定(只会收缩详情栏,然后将其自动关闭);关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。
|
||||
外壳插件:三栏 AppFrame(拖动手柄与让步链)加 `ctx.layout` 面板几何服务;它注册到运行时拥有的 `root` slot,并声明 `sidebar`、`conversation`、`details` 和 `conversation.empty`。侧边栏宽度固定(只会收缩详情栏,然后将其自动关闭);关闭的侧边栏仍保留 56px 控制轨道,详情栏则关闭到零宽度。该包还提供主题呈现器:它消费解析后的 `ctx.theme` 快照,并将其投影到 `document.body`(依据当前配色方案设置 `data-ds-dark-theme`,并将主题的别名 token 设为内联变量)。
|
||||
|
||||
AppFrame 读取运行时 Session 投影:`baselinesReady` 选择加载状态,页面局部的 `SessionListState.intent` 选择空白编辑器,已连接 Session 则通过 `SessionProvider` 渲染。会话及空状态的 owner share 为空;每个注册方通过标准 hook 获取业务数据,并从自身的 inject 表层获取操作。侧边栏 owner share 只包含 `collapsed` 和 `width`;导航操作属于侧边栏自身注入的服务表层。
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
},
|
||||
"dshClient": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-runtime"
|
||||
"@deepseek-ai/dsh-client-runtime",
|
||||
"@deepseek-ai/dsh-client-ui-theme"
|
||||
],
|
||||
"platform": "web"
|
||||
},
|
||||
@@ -36,13 +37,16 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
|
||||
"@deepseek-ai/dsh-client-ui-theme": "^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-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",
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
* four child slots (declaration = exclusive render authority), seats the
|
||||
* layout store (panel geometry), and wires the panel-action service face.
|
||||
* ctx.layout is the cross-plugin panel-action seam; navigation state lives
|
||||
* with the runtime sessions service.
|
||||
* with the runtime sessions service. A second effect seats the theme
|
||||
* presenter, which projects ctx.theme snapshots onto document.body.
|
||||
*/
|
||||
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
import type { PanelActions } from './service.ts'
|
||||
import { AppFrame } from './AppFrame.tsx'
|
||||
import { createLayoutStore } from './stores.ts'
|
||||
import { LayoutService } from './service.ts'
|
||||
import { ThemePresenter } from './theme-presenter.ts'
|
||||
|
||||
// Contract surface only (export-convergence rule: cross-package consumers
|
||||
// keep a symbol exported; test-only/package-internal symbols live off /src).
|
||||
@@ -62,7 +65,7 @@ export interface DetailsOwnerProps {}
|
||||
export interface EmptyOwnerProps { children?: never }
|
||||
|
||||
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
|
||||
export const inject = ['slots']
|
||||
export const inject = ['slots', 'theme']
|
||||
|
||||
/**
|
||||
* Client plugin body: provide ctx.layout, then one register() call — AppFrame
|
||||
@@ -98,4 +101,16 @@ export function apply(ctx: ClientContext): void {
|
||||
void disposeService()
|
||||
}
|
||||
}, 'ui-layout: service + root registration')
|
||||
|
||||
// Theme presentation: pure DOM writes from resolved snapshots — initial
|
||||
// state through the getter once, then event-driven only; no React path.
|
||||
ctx.effect(() => {
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(ctx.theme.getTheme())
|
||||
const off = ctx.on('theme/change', (snapshot) => { presenter.apply(snapshot) })
|
||||
return () => {
|
||||
off()
|
||||
presenter.dispose()
|
||||
}
|
||||
}, 'ui-layout: theme presenter')
|
||||
}
|
||||
|
||||
43
packages/client/ui-layout/src/client/theme-presenter.ts
Normal file
43
packages/client/ui-layout/src/client/theme-presenter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Global theme DOM applier: projects the resolved ThemeSnapshot onto
|
||||
* document.body — the `data-ds-dark-theme` palette switch plus the active
|
||||
* theme's alias-token overrides as inline CSS variables. Pure DOM writes, no
|
||||
* React involvement; the presenter only ever retracts what it wrote itself,
|
||||
* so foreign body attributes and inline styles survive apply/dispose.
|
||||
*/
|
||||
import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
|
||||
/** Body attribute selecting the dark base palette in the token stylesheets. */
|
||||
export const DARK_ATTRIBUTE = 'data-ds-dark-theme'
|
||||
|
||||
/** Applies theme snapshots to document.body; one instance per plugin fiber. */
|
||||
export class ThemePresenter {
|
||||
/** Token names this presenter wrote in the last apply (its retraction set). */
|
||||
private appliedTokens: string[] = []
|
||||
|
||||
/**
|
||||
* Project a snapshot onto the body: switch the palette attribute from
|
||||
* `active.colorScheme` (never the id — `system` is resolved upstream) and
|
||||
* replace the previously applied token variables with `active.tokens`.
|
||||
* @param snapshot - resolved theme snapshot from ctx.theme.
|
||||
*/
|
||||
apply(snapshot: ThemeSnapshot): void {
|
||||
const body = document.body
|
||||
if (snapshot.active.colorScheme === 'dark') body.setAttribute(DARK_ATTRIBUTE, '')
|
||||
else body.removeAttribute(DARK_ATTRIBUTE)
|
||||
for (const name of this.appliedTokens) body.style.removeProperty(name)
|
||||
this.appliedTokens = []
|
||||
for (const [name, value] of Object.entries(snapshot.active.tokens)) {
|
||||
body.style.setProperty(name, value)
|
||||
this.appliedTokens.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
/** Retract everything this presenter wrote: the palette attribute and all applied token variables. */
|
||||
dispose(): void {
|
||||
const body = document.body
|
||||
body.removeAttribute(DARK_ATTRIBUTE)
|
||||
for (const name of this.appliedTokens) body.style.removeProperty(name)
|
||||
this.appliedTokens = []
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
import { Context } from '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 { apply as themeApply, inject as themeInject, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
import { apply, inject, LayoutService } from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import { apply as nodeApply } from '@deepseek-ai/dsh-client-ui-layout'
|
||||
import * as invariant from '@deepseek-ai/dsh-client-ui-layout/invariant'
|
||||
@@ -16,13 +18,17 @@ import * as invariant from '@deepseek-ai/dsh-client-ui-layout/invariant'
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
const slotsFiber = ctx.plugin(SlotsService)
|
||||
// Theme now injects ['slots', 'locale'] (it registers its Appearance
|
||||
// settings row); seat a real locale service so the theme fiber activates.
|
||||
ctx.provide('locale', new LocaleService(ctx))
|
||||
await ctx.plugin({ inject: themeInject, apply: themeApply }).await()
|
||||
await slotsFiber.await()
|
||||
return { ctx, slots: ctx.get('slots') as SlotsService }
|
||||
}
|
||||
|
||||
describe('ui-layout client apply', () => {
|
||||
it('declares its service dependencies', () => {
|
||||
expect(inject).toEqual(['slots'])
|
||||
expect(inject).toEqual(['slots', 'theme'])
|
||||
})
|
||||
|
||||
it('provides ctx.layout and registers AppFrame into root with the four child declarations', async () => {
|
||||
@@ -53,6 +59,23 @@ describe('ui-layout client apply', () => {
|
||||
expect(actions.toggleSidebar).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('theme presenter applies the initial snapshot, follows theme/change, and unwinds on dispose', async () => {
|
||||
const { ctx } = await bench()
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
// Initial getter application: jsdom has no matchMedia, system resolves light.
|
||||
expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false)
|
||||
const theme = ctx.get('theme') as ThemeService
|
||||
theme.setTheme('dark')
|
||||
expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(true)
|
||||
await fiber.dispose()
|
||||
expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false)
|
||||
// Listener is off: further theme changes no longer reach the body.
|
||||
theme.setTheme('light')
|
||||
theme.setTheme('dark')
|
||||
expect(document.body.hasAttribute('data-ds-dark-theme')).toBe(false)
|
||||
})
|
||||
|
||||
it('teardown unwinds the service, the root registration, and the child declarations', async () => {
|
||||
const { ctx, slots } = await bench()
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
|
||||
56
packages/client/ui-layout/tests/theme-presenter.spec.ts
Normal file
56
packages/client/ui-layout/tests/theme-presenter.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// @vitest-environment jsdom
|
||||
// ThemePresenter behavior account: the palette attribute follows
|
||||
// active.colorScheme only, token variables replace the previous apply's set,
|
||||
// and dispose retracts everything the presenter wrote.
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { ThemeSnapshot } from '@deepseek-ai/dsh-client-ui-theme/client'
|
||||
import { DARK_ATTRIBUTE, ThemePresenter } from '@deepseek-ai/dsh-client-ui-layout/src/client/theme-presenter.ts'
|
||||
|
||||
function snapshot(colorScheme: 'light' | 'dark', tokens: Record<string, string> = {}): ThemeSnapshot {
|
||||
// The presenter must key off colorScheme, not the id — keep them distinct.
|
||||
const active = { id: `${colorScheme}-test`, colorScheme, tokens }
|
||||
return { preference: colorScheme, active, themes: [active], revision: 1 }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.removeAttribute(DARK_ATTRIBUTE)
|
||||
document.body.removeAttribute('style')
|
||||
})
|
||||
|
||||
describe('ThemePresenter', () => {
|
||||
it('light scheme leaves the dark attribute absent', () => {
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(snapshot('light'))
|
||||
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
|
||||
})
|
||||
|
||||
it('dark scheme sets the attribute; switching back to light removes it', () => {
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(snapshot('dark'))
|
||||
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(true)
|
||||
presenter.apply(snapshot('light'))
|
||||
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
|
||||
})
|
||||
|
||||
it('applies tokens as inline variables and clears the previous set on theme change', () => {
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(snapshot('dark', { '--dsw-alias-bg': '#111', '--dsw-alias-fg': '#eee' }))
|
||||
expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('#111')
|
||||
expect(document.body.style.getPropertyValue('--dsw-alias-fg')).toBe('#eee')
|
||||
presenter.apply(snapshot('light', { '--dsw-alias-bg': '#fff' }))
|
||||
expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('#fff')
|
||||
// The old theme's extra variable is gone, not merged.
|
||||
expect(document.body.style.getPropertyValue('--dsw-alias-fg')).toBe('')
|
||||
})
|
||||
|
||||
it('dispose removes the attribute and every applied variable, sparing foreign inline styles', () => {
|
||||
document.body.style.setProperty('--foreign', 'kept')
|
||||
const presenter = new ThemePresenter()
|
||||
presenter.apply(snapshot('dark', { '--dsw-alias-bg': '#111' }))
|
||||
presenter.dispose()
|
||||
expect(document.body.hasAttribute(DARK_ATTRIBUTE)).toBe(false)
|
||||
expect(document.body.style.getPropertyValue('--dsw-alias-bg')).toBe('')
|
||||
expect(document.body.style.getPropertyValue('--foreign')).toBe('kept')
|
||||
})
|
||||
})
|
||||
@@ -11,9 +11,15 @@
|
||||
{
|
||||
"path": "../../../vendor/cordis"
|
||||
},
|
||||
{
|
||||
"path": "../locale"
|
||||
},
|
||||
{
|
||||
"path": "../ui-slots"
|
||||
},
|
||||
{
|
||||
"path": "../ui-theme"
|
||||
},
|
||||
{
|
||||
"path": "../ui-primitives"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user