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:
Tianyi Cui
2026-07-26 14:37:46 +08:00
136 files changed
+4696 -459

No files matched your search

@@ -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
README.md: c392d745021c0fc6a752cf71dd0506a435106c50
README.zh.md: 83ab81e01eae435a74b50fa363a4de203c483002
@@ -0,0 +1,17 @@
# @deepseek-ai/dsh-client-ui-settings-general
English | [中文](README.zh.md)
Settings ownerless-copy plugin: registers everything on the Settings surface that belongs to no single feature — the shell's trigger/header/close chrome content, the General section (Permission/Tool Call skeleton rows + the `settings.general.item` slot declaration), and the `settings` dictionaries. Feature-owned rows (Language, Appearance) and sections (Models) stay with their feature packages.
## Model Experience
None, as the plugin renders browser settings UI; nothing here reaches a model request.
#### KV Cache effect
None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Permission and Tool Call are display skeletons** — the backing host services and RPC methods do not exist yet; the controls are disabled and write nothing. When they gain real backing, each moves to its owning feature plugin per the self-registration doctrine.
@@ -0,0 +1,17 @@
# @deepseek-ai/dsh-client-ui-settings-general
[English](README.md) | 中文
设置界面文案插件:在设置界面注册所有不属于单一功能的内容,包括外壳的触发器、标题栏与关闭控件内容,「通用」分区(「权限」/「工具调用」骨架行和 `settings.general.item` slot 声明),以及 `settings` 字典。归具体功能所有的行(「语言」、「外观」)和分区(「模型」)仍由各自的功能包提供。
## 模型体验
无。该插件渲染浏览器设置 UI;这里没有任何内容进入模型请求。
#### KV Cache 影响
无;该包既不组装也不发送提供方请求。
## 已知限制与暂缓事项
- **「权限」与「工具调用」只是展示骨架**:对应的宿主服务和 RPC 方法尚不存在;这些控件已禁用,不会写入任何内容。一旦获得实际支撑,按照自注册原则,每一项都会移至拥有它的功能插件。
@@ -0,0 +1,67 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings-general",
"description": "Settings ownerless-copy plugin: the General section (skeleton rows + item slot), the shell trigger/header chrome content, and the settings dictionaries",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-locale"
],
"platform": "web"
},
"scripts": {
"bundle": "tsdown",
"watch": "tsdown --watch"
},
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-client-locale": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^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-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-settings": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@types/react": "~18.3.1",
"cordis": "^4.0.0-rc.7",
"react": "^18.2.0"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
]
}
@@ -0,0 +1,111 @@
/* General section rows (figma 501:29983 'Options'): stacked groups, 16px
* vertical padding each, hairline separator under all but the last child
* (feature-contributed rows carry their own row chrome and separators; the
* :last-child rule strips the trailing one wherever the column ends). */
.section {
display: flex;
flex-direction: column;
width: 100%;
}
.section > :last-child {
border-bottom: none;
}
/* Title + trailing control row (figma 'Setting-Cell': gap 8, pad 16/0). */
.row {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 0;
border-bottom: 1px solid var(--dsw-alias-border-l2);
}
/* Title + full-width body group (figma 'Frame 2117131229': column, gap 8). */
.group {
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 0;
border-bottom: 1px solid var(--dsw-alias-border-l2);
}
/* Leading text column (figma 'Frame 2036083120': gap 4, pad-right 48). */
.rowText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
padding-right: 48px;
}
.title {
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: var(--dsw-alias-label-primary);
}
.desc {
font-size: 12px;
font-weight: 400;
line-height: 18px;
color: var(--dsw-alias-label-tertiary);
}
/* Selector pill (figma 'Selector': h36 r18, fill #F5F6F7, pad 0/14, gap 12). */
.selector {
display: inline-flex;
align-items: center;
gap: 12px;
height: 36px;
padding: 0 14px;
border: none;
border-radius: 18px;
background: var(--dsw-alias-bg-module-platform);
font: inherit;
font-size: 14px;
line-height: 22px;
color: var(--dsw-alias-label-primary);
cursor: pointer;
}
.selector:disabled {
cursor: default;
}
.chevron {
flex: none;
}
/* Tool Call mode cubes share an 8px gap. */
.cubeRow {
display: flex;
align-items: stretch;
gap: 8px;
}
/* Tool Call mode cube (figma '.Selector Cube' 418w r16; horizontal inset =
* outer pad 4 + inner .Menu_cell pad 10, vertical = inner pad 8). */
.modeCube {
box-sizing: border-box;
width: 418px;
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
padding: 8px 14px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 16px;
background: transparent;
text-align: left;
}
/* Selected cube: #F5F6F7 fill + #ADB2B8 border (static token — the bluish-400
* step has no alias-layer name). */
.selected {
background: var(--dsw-alias-bg-module-platform);
border-color: var(--dsw-static-neutral-bluish-400);
}
@@ -0,0 +1,61 @@
/**
* The General section (figma 501:29983 'Options'): Permission and Tool Call
* skeleton rows, then the feature-contributed preference rows from the
* `settings.general.item` slot (locale → Language, ui-theme → Appearance).
* The section column stacks rows; each row draws its own internals and
* separator.
*/
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import css from './GeneralSection.module.css'
/** Injected face of the General section: the settings-namespace translate. */
export interface GeneralSectionInjected {
/** Translate a `settings` dictionary key to the active-locale text. */
t: (key: string) => string
}
/** Full component props: section owner share + item render share + inject face. */
export type GeneralSectionComponentProps =
PropsRuntime<'settings.section'> & PropsRenderSlots<'settings.general.item'> & GeneralSectionInjected
/**
* Render the General section content column.
* @param props - composed slot props (contract/slots.ts).
* @returns the section element tree.
*/
export function GeneralSection({ t, renderSlot }: GeneralSectionComponentProps) {
return (
<div className={css.section}>
{/* Permission (skeleton): disabled selector pill. */}
<div className={css.row}>
<div className={css.rowText}>
<div className={css.title}>{t('permission.title')}</div>
<div className={css.desc}>{t('permission.desc')}</div>
</div>
<button type="button" className={css.selector} disabled>
{t('permission.value')}
<IconChevronDownOutline14 className={css.chevron} />
</button>
</div>
{/* Tool Call (skeleton): schema cube pinned selected, code cube unselected. */}
<div className={css.group}>
<div className={css.title}>{t('toolcall.title')}</div>
<div className={css.cubeRow}>
<div className={`${css.modeCube} ${css.selected}`}>
<div className={css.title}>{t('toolcall.schema.title')}</div>
<div className={css.desc}>{t('toolcall.schema.desc')}</div>
</div>
<div className={css.modeCube}>
<div className={css.title}>{t('toolcall.code.title')}</div>
<div className={css.desc}>{t('toolcall.code.desc')}</div>
</div>
</div>
</div>
{/* Feature-owned preference rows (Language, Appearance, …). */}
{renderSlot('settings.general.item', {})}
</div>
)
}
@@ -0,0 +1,7 @@
/* Trigger row label (the shell's button provides layout/colors; the label
* only guards against overflow during the sidebar collapse crossfade). */
.triggerLabel {
overflow: hidden;
white-space: nowrap;
}
@@ -0,0 +1,56 @@
/**
* Shell chrome content registered into the shell's trigger/header seats: the
* trigger row icon + label (figma sidebar foot) and the panel title text.
* The shell renders the surrounding chrome (button, nav heading row) and
* reads each entry's `label` option for aria text.
*/
import { IconSettingsOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import css from './chrome.module.css'
/** Injected face of both chrome seats: the settings-namespace translate. */
export interface ChromeInjected {
/** Translate a `settings` dictionary key to the active-locale text. */
t: (key: string) => string
}
/** Trigger content props: the sidebar column state + translate. */
export type TriggerContentProps = PropsRuntime<'settings.trigger'> & ChromeInjected
/** Header content props: translate only. */
export type HeaderContentProps = PropsRuntime<'settings.header'> & ChromeInjected
/**
* Render the trigger row content (icon; label only in the wide column).
* @param props - composed slot props.
* @returns the trigger content fragment.
*/
export function TriggerContent({ wide, t }: TriggerContentProps) {
return (
<>
<IconSettingsOutline14 size={wide ? 14 : 18} />
{wide && <span className={css.triggerLabel}>{t('trigger')}</span>}
</>
)
}
/**
* Render the panel title text.
* @param props - composed slot props.
* @returns the title text node.
*/
export function HeaderContent({ t }: HeaderContentProps) {
return <>{t('title')}</>
}
/** Close-button label text props: translate only. */
export type CloseLabelProps = PropsRuntime<'settings.close'> & ChromeInjected
/**
* Render the close button's visually-hidden label text.
* @param props - composed slot props.
* @returns the label text node.
*/
export function CloseLabel({ t }: CloseLabelProps) {
return <>{t('close')}</>
}
@@ -0,0 +1,87 @@
/**
* Settings ownerless-copy plugin, browser half: registers everything on the
* Settings surface that belongs to no single feature — the trigger/header
* chrome content, the General section (skeleton rows + the
* `settings.general.item` slot declaration), and the `settings`
* dictionaries. Feature-owned rows and sections stay with their features.
* Export discipline: packages/client/AGENTS.md.
*/
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
// Type-only: pulls the shell's SlotMap merges (trigger/header/section/item).
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import type { ChromeInjected } from './chrome.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
import type { GeneralSectionInjected } from './GeneralSection.tsx'
import { GeneralSection } from './GeneralSection.tsx'
import { en, zh } from './locales.ts'
export type {
ChromeInjected, CloseLabelProps, HeaderContentProps, TriggerContentProps,
} from './chrome.tsx'
export type {
GeneralSectionComponentProps, GeneralSectionInjected,
} from './GeneralSection.tsx'
/** Dictionary namespace owned by this plugin (shell chrome + General copy). */
const NS = 'settings'
/**
* Required services (cordis fiber inject). The target slots are 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 `settings` dictionaries, the chrome content, and the General
* section, each once its slot declaration is on the ledger.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.effect(() => {
const disposers = [
ctx.locale.register(NS, 'zh', zh),
ctx.locale.register(NS, 'en', en),
]
return () => { for (const dispose of disposers) dispose() }
}, 'ui-settings-general: dictionaries')
const t = ctx.locale.bind(NS)
const chromeInjected = (): ChromeInjected => ({ t })
const generalInjected = (): GeneralSectionInjected => ({ t })
// All four seats refresh on locale change: re-registration bumps each
// slot's ledger version, which re-renders the outlets through their own
// subscriptions (outlet memoization would swallow a parent-only render).
ctx.effect(() => {
const trigger = deferRegistration(ctx.slots, 'settings.trigger', TriggerContent, () =>
ctx.slots.register({ name: 'settings.trigger', inject: chromeInjected }, TriggerContent))
const header = deferRegistration(ctx.slots, 'settings.header', HeaderContent, () =>
ctx.slots.register({ name: 'settings.header', inject: chromeInjected }, HeaderContent))
const close = deferRegistration(ctx.slots, 'settings.close', CloseLabel, () =>
ctx.slots.register({ name: 'settings.close', inject: chromeInjected }, CloseLabel))
const general = deferRegistration(ctx.slots, 'settings.section', GeneralSection, () =>
ctx.slots.register({
name: 'settings.section',
id: 'general',
order: 0,
label: t('general.nav'),
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
inject: generalInjected,
}, GeneralSection))
const offLocale = ctx.on('locale/change', () => {
trigger.refresh()
header.refresh()
close.refresh()
general.refresh()
})
return () => {
offLocale()
trigger.dispose()
header.dispose()
close.dispose()
general.dispose()
}
}, 'ui-settings-general: chrome and section registrations')
}
@@ -0,0 +1,40 @@
/**
* `settings` namespace dictionaries: shell chrome plus the shell-owned
* General section (nav label, skeleton rows). Skeleton-row technical copy
* (Read only / Schema mode / Code mode and their descriptions) is shared
* verbatim across locales per the Figma design. Feature-owned rows
* (Language, Appearance) ship their copy in their own packages.
*/
import type { LocaleDict } from '@deepseek-ai/dsh-client-locale/client'
const SHARED = {
'permission.value': 'Read only',
'toolcall.schema.title': 'Schema mode',
'toolcall.schema.desc': 'Traditional function calling — invoke tools one at a time',
'toolcall.code.title': 'Code mode',
'toolcall.code.desc': 'Chain multiple tools with code — multi-step orchestration',
} satisfies LocaleDict
/** Simplified Chinese dictionary. */
export const zh: LocaleDict = {
...SHARED,
'trigger': '设置',
'title': '设置',
'close': '关闭',
'general.nav': '通用设置',
'permission.title': '权限',
'permission.desc': '选择默认权限模式',
'toolcall.title': '工具调用',
}
/** English dictionary. */
export const en: LocaleDict = {
...SHARED,
'trigger': 'Settings',
'title': 'Settings',
'close': 'Close',
'general.nav': 'General',
'permission.title': 'Permission',
'permission.desc': 'Choose default permission mode',
'toolcall.title': 'Tool Call',
}
@@ -0,0 +1,6 @@
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.css'
@@ -0,0 +1,4 @@
/** Host loader entry for the browser implementation exported from `./client`. */
/** Host plugin body — no host-side behavior for the general settings plugin. */
export function apply(): void {}
@@ -0,0 +1,32 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-client-ui-settings-general`.
* @module @deepseek-ai/dsh-client-ui-settings-general/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-settings-general'
/** Cordis companion plugin name. */
export const name = 'client-ui-settings-general-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: a copy-owning registrant contributing chrome content
* and the General section into shell-declared slots — it emits no cordis
* events and owns no cross-plugin mutable relation; slot conflicts already
* fail loud in the slot core at load time.
*/
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 */
@@ -0,0 +1,150 @@
/** Ownerless-copy registrations: the four seats, the dictionaries, locale refresh, and HMR recovery. */
import { Context } from 'cordis'
import { describe, expect, it } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
import type { GeneralSectionInjected } from '@deepseek-ai/dsh-client-ui-settings-general/client'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
/** The four seats this plugin fills (slot name → expected component). */
const SEATS = [
['settings.trigger', TriggerContent],
['settings.header', HeaderContent],
['settings.close', CloseLabel],
['settings.section', GeneralSection],
] as const
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
const locale = new LocaleService(ctx)
ctx.provide('locale', locale)
return { ctx, slots: ctx.get('slots') as SlotsService, locale }
}
/** Declare the shell's four child slots the way ui-settings' entry does. */
function declare(slots: SlotsService): () => void {
return slots.register(
{
name: 'root',
children: {
'settings.trigger': { kind: 'single', scope: 'root' },
'settings.header': { kind: 'single', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
},
} as never,
() => null,
)
}
function generalEntry(slots: SlotsService) {
return slots.entries('settings.section').find(e => e.component === GeneralSection)
}
describe('ui-settings-general apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale'])
})
it('fills all four seats for declarations before or after apply', async () => {
const before = await bench()
declare(before.slots)
await before.ctx.plugin({ inject: [...inject], apply }).await()
for (const [name, component] of SEATS) {
expect(before.slots.entries(name)[0]!.component).toBe(component)
}
const entry = generalEntry(before.slots)!
expect(entry.options).toEqual({ id: 'general', order: 0, label: '通用设置' })
expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
const injected = (entry.inject as unknown as () => GeneralSectionInjected)()
expect(injected.t('permission.title')).toBe('权限')
// The chrome seats share one inject face: the settings-ns translate.
const chrome = (before.slots.entries('settings.trigger')[0]!.inject as unknown as () => GeneralSectionInjected)()
expect(chrome.t('trigger')).toBe('设置')
const after = await bench()
await after.ctx.plugin({ inject: [...inject], apply }).await()
for (const [name] of SEATS) expect(after.slots.entries(name)).toHaveLength(0)
declare(after.slots)
await Promise.resolve()
for (const [name, component] of SEATS) {
expect(after.slots.entries(name)[0]!.component).toBe(component)
// The self-inflicted ledger notifications hit the duplicate guard.
expect(after.slots.entries(name)).toHaveLength(1)
}
})
it('registers the zh/en settings dictionaries and frees the seats on teardown', async () => {
const b = await bench()
declare(b.slots)
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(b.locale.bind('settings')('title')).toBe('设置')
b.locale.setLocale('en')
expect(b.locale.bind('settings')('close')).toBe('Close')
b.locale.setLocale('zh')
await fiber.dispose()
// The (ns, locale) seats are free again — the dictionary disposers ran.
expect(() => b.locale.register('settings', 'zh', {})).not.toThrow()
expect(() => b.locale.register('settings', 'en', {})).not.toThrow()
})
it('refreshes all four seats on locale change with fresh General label text', async () => {
const b = await bench()
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const zhVersions = SEATS.map(([name]) => b.slots.getVersion(name))
b.locale.setLocale('en')
// Every seat re-registered (version moved) and the label re-resolved.
SEATS.forEach(([name], i) => {
expect(b.slots.getVersion(name)).toBeGreaterThan(zhVersions[i]!)
expect(b.slots.entries(name)).toHaveLength(1)
})
expect(generalEntry(b.slots)!.options.label).toBe('General')
b.locale.setLocale('zh')
expect(generalEntry(b.slots)!.options.label).toBe('通用设置')
})
it('locale change while the slots are undeclared stays a no-op', async () => {
const b = await bench()
await b.ctx.plugin({ inject: [...inject], apply }).await()
b.locale.setLocale('en')
for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
b.locale.setLocale('zh')
})
it('re-registers after an HMR collapse of the declaring chain (stale disposers must not block)', async () => {
const b = await bench()
const redeclare = declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
// Declarer unload: the cascade removes every seat entry and the item
// declaration while our local disposers go stale.
redeclare()
for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
expect(b.slots.spec('settings.general.item')).toBeUndefined()
declare(b.slots)
await Promise.resolve()
for (const [name, component] of SEATS) {
expect(b.slots.entries(name)[0]!.component).toBe(component)
}
expect(b.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
// The recovered registrations still ride the locale path.
b.locale.setLocale('en')
expect(generalEntry(b.slots)!.options.label).toBe('General')
b.locale.setLocale('zh')
})
it('removes every seat and the item declaration on teardown', async () => {
const b = await bench()
declare(b.slots)
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(b.slots.spec('settings.general.item')).toBeDefined()
await fiber.dispose()
for (const [name] of SEATS) expect(b.slots.entries(name)).toHaveLength(0)
expect(b.slots.spec('settings.general.item')).toBeUndefined()
})
})
@@ -0,0 +1,72 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import { en } from '../src/client/locales.ts'
afterEach(cleanup)
const t = (key: string) => en[key] ?? key
// Global standard kit stubs: none of these components consume the hooks.
const unusedHook = (() => { throw new Error('unused by settings-general components') }) as never
const kit = { useSessions: unusedHook, useWorkspaces: unusedHook }
describe('chrome content', () => {
it('TriggerContent renders the icon with the label in the wide column', () => {
const { container } = render(<TriggerContent {...kit} wide t={t} />)
expect(container.querySelector('svg')).toBeTruthy()
expect(screen.getByText('Settings')).toBeTruthy()
})
it('TriggerContent drops the label in the rail state', () => {
const { container } = render(<TriggerContent {...kit} wide={false} t={t} />)
expect(container.querySelector('svg')).toBeTruthy()
expect(screen.queryByText('Settings')).toBeNull()
})
it('HeaderContent and CloseLabel render their translated text', () => {
render(<HeaderContent {...kit} t={t} />)
render(<CloseLabel {...kit} t={t} />)
expect(screen.getByText('Settings')).toBeTruthy()
expect(screen.getByText('Close')).toBeTruthy()
})
})
describe('GeneralSection', () => {
function mount() {
const renderSlot = vi.fn(
((key: string) => <div data-testid={`slot-${key}`} />) as GeneralSectionComponentProps['renderSlot'],
)
const props: GeneralSectionComponentProps = { ...kit, t, renderSlot }
const view = render(<GeneralSection {...props} />)
return { view, renderSlot }
}
it('renders the Permission skeleton row with the disabled selector', () => {
mount()
expect(screen.getByText('Permission')).toBeTruthy()
expect(screen.getByText('Choose default permission mode')).toBeTruthy()
const selector = screen.getByRole('button', { name: /Read only/ }) as HTMLButtonElement
expect(selector.disabled).toBe(true)
})
it('renders the Tool Call skeleton cubes with schema pinned selected', () => {
mount()
expect(screen.getByText('Tool Call')).toBeTruthy()
const schema = screen.getByText('Schema mode')
const code = screen.getByText('Code mode')
expect(schema.parentElement!.className).toContain('selected')
expect(code.parentElement!.className).not.toContain('selected')
expect(screen.getByText('Traditional function calling — invoke tools one at a time')).toBeTruthy()
expect(screen.getByText('Chain multiple tools with code — multi-step orchestration')).toBeTruthy()
})
it('renders the feature-contributed item slot after the skeleton rows', () => {
const { renderSlot } = mount()
expect(renderSlot).toHaveBeenCalledWith('settings.general.item', {})
expect(screen.getByTestId('slot-settings.general.item')).toBeTruthy()
})
})
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import * as GeneralInvariant from '@deepseek-ai/dsh-client-ui-settings-general/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
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 expect(ctx.plugin(GeneralInvariant).await()).resolves.toBeDefined()
})
it('node-half apply is a no-op host placeholder', async () => {
const { apply } = await import('@deepseek-ai/dsh-client-ui-settings-general')
apply()
expect(true).toBe(true) // reaching here without throw is the contract
})
})
@@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../ui-slots"
},
{
"path": "../ui-primitives"
},
{
"path": "../runtime"
},
{
"path": "../ui-settings"
},
{
"path": "../locale"
},
{
"path": "../../support/invariants"
}
]
}
@@ -0,0 +1,3 @@
import { clientBundle } from '../tsdown.client.ts'
export default clientBundle('@deepseek-ai/dsh-client-ui-settings-general', ['lib/types/index.js', 'lib/types/invariant.js'])