Merge remote-tracking branch 'origin/master' into feature/gui-first-run-welcome

# Conflicts:
#	packages/client/ui-settings-general/src/client/index.ts
#	packages/client/ui-settings-general/src/client/locales.ts
#	packages/client/ui-settings-general/tests/apply.spec.ts
#	packages/client/ui-settings/README.i18n.yaml
#	packages/client/ui-settings/README.md
#	packages/client/ui-settings/README.zh.md
#	packages/client/ui-settings/src/client/index.ts
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/src/api-proxy.ts
This commit is contained in:
NI0317
2026-07-31 12:48:39 +08:00
462 changed files with 12006 additions and 2484 deletions
@@ -6,18 +6,12 @@
* separator.
*/
import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { PropsLocale, 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. */
/** Full component props: section owner share + item render share + the standard locale seat. */
export type GeneralSectionComponentProps =
PropsRuntime<'settings.section'> & PropsRenderSlots<'settings.general.item'> & GeneralSectionInjected
PropsRuntime<'settings.section'> & PropsRenderSlots<'settings.general.item'> & PropsLocale<'settings'>
/**
* Render the General section content column.
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef } from 'react'
import type { ReactNode } from 'react'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import { BrandWordmark, Button } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import type { WelcomeNoticeState, WelcomeNoticeStore } from './welcome-store.ts'
@@ -25,11 +25,11 @@ function emphasizedFeedback(paragraph: string, emphasis: string): ReactNode {
export interface WelcomeNoticeInjected {
controller: WelcomeNoticeStore
useSnapshot: SnapshotSelectorHook<WelcomeNoticeState>
t: (key: string) => string
}
/** Coordinator owner props plus the welcome step's injected face. */
export type WelcomeNoticeProps = PropsRuntime<'settings.onboarding'> & WelcomeNoticeInjected
export type WelcomeNoticeProps =
PropsRuntime<'settings.onboarding'> & PropsLocale<'settings'> & WelcomeNoticeInjected
/** Render the mandatory notice until its current version commits durably. */
export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
@@ -5,20 +5,14 @@
* 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 type { PropsLocale, 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 + the standard locale seat. */
export type TriggerContentProps = PropsRuntime<'settings.trigger'> & PropsLocale<'settings'>
/** 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
/** Header content props: the standard locale seat only. */
export type HeaderContentProps = PropsRuntime<'settings.header'> & PropsLocale<'settings'>
/**
* Render the trigger row content (icon; label only in the wide column).
@@ -43,8 +37,8 @@ export function HeaderContent({ t }: HeaderContentProps) {
return <>{t('title')}</>
}
/** Close-button label text props: translate only. */
export type CloseLabelProps = PropsRuntime<'settings.close'> & ChromeInjected
/** Close-button label text props: the standard locale seat only. */
export type CloseLabelProps = PropsRuntime<'settings.close'> & PropsLocale<'settings'>
/**
* Render the close button's visually-hidden label text.
@@ -12,24 +12,30 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
// 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'
// Type-only: pulls ctx.locale and the 'settings.general.item' SlotMap merge.
import type {} from '@deepseek-ai/dsh-client-locale/client'
import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
import type { GeneralSectionInjected } from './GeneralSection.tsx'
import { GeneralSection } from './GeneralSection.tsx'
import type { WelcomeNoticeInjected } from './WelcomeNotice.tsx'
import { WelcomeNotice } from './WelcomeNotice.tsx'
import { refreshWelcomeIfLoaded, WelcomeNoticeStore } from './welcome-store.ts'
import { en, zh } from './locales.ts'
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts'
import { en, zh, type SettingsKey } from './locales.ts'
export type {
ChromeInjected, CloseLabelProps, HeaderContentProps, TriggerContentProps,
CloseLabelProps, HeaderContentProps, TriggerContentProps,
} from './chrome.tsx'
export type {
GeneralSectionComponentProps, GeneralSectionInjected,
} from './GeneralSection.tsx'
export type { GeneralSectionComponentProps } from './GeneralSection.tsx'
export type { WelcomeNoticeInjected, WelcomeNoticeProps } from './WelcomeNotice.tsx'
export type { WelcomeNoticeState } from './welcome-store.ts'
export type { SettingsKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** Shell chrome + shell-owned General section copy. */
settings: SettingsKey
}
}
/** Dictionary namespace owned by this plugin (shell chrome + General copy). */
const NS = 'settings'
@@ -47,24 +53,18 @@ export const inject = ['slots', 'locale', 'connection']
* @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')
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-settings-general: dictionaries')
// Copy freshness is framework-owned: components read the standard `t`
// seat, and the nav label is a thunk the owner resolves per render — no
// locale/change re-registration wiring.
const t = ctx.locale.bind(NS)
const connection = ctx.get('connection') as ConnectionHandle
const welcomeController = new WelcomeNoticeStore(connection.api)
const useWelcomeSnapshot = bindSnapshotSelector(welcomeController.store)
const chromeInjected = (): ChromeInjected => ({ t })
const generalInjected = (): GeneralSectionInjected => ({ t })
const welcomeInjected = (): WelcomeNoticeInjected => ({
controller: welcomeController,
useSnapshot: useWelcomeSnapshot,
t,
})
ctx.effect(() => {
@@ -78,42 +78,31 @@ export function apply(ctx: ClientContext): void {
]
return () => { for (const dispose of disposers) dispose() }
}, 'ui-settings-general: welcome invalidations')
// 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))
ctx.slots.register({ name: 'settings.trigger', locale: NS }, TriggerContent))
const header = deferRegistration(ctx.slots, 'settings.header', HeaderContent, () =>
ctx.slots.register({ name: 'settings.header', inject: chromeInjected }, HeaderContent))
ctx.slots.register({ name: 'settings.header', locale: NS }, HeaderContent))
const close = deferRegistration(ctx.slots, 'settings.close', CloseLabel, () =>
ctx.slots.register({ name: 'settings.close', inject: chromeInjected }, CloseLabel))
ctx.slots.register({ name: 'settings.close', locale: NS }, CloseLabel))
const general = deferRegistration(ctx.slots, 'settings.section', GeneralSection, () =>
ctx.slots.register({
name: 'settings.section',
id: 'general',
order: 0,
label: t('general.nav'),
label: () => t('general.nav'),
locale: NS,
children: { 'settings.general.item': { kind: 'list', scope: 'root' } },
inject: generalInjected,
}, GeneralSection))
const welcome = deferRegistration(ctx.slots, 'settings.onboarding', WelcomeNotice, () =>
ctx.slots.register({
name: 'settings.onboarding',
id: 'welcome-notice',
order: -100,
locale: NS,
inject: welcomeInjected,
}, WelcomeNotice))
const offLocale = ctx.on('locale/change', () => {
trigger.refresh()
header.refresh()
close.refresh()
general.refresh()
welcome.refresh()
})
return () => {
offLocale()
trigger.dispose()
header.dispose()
close.dispose()
@@ -5,19 +5,17 @@
* 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'
import { WELCOME_NOTICE_COPY } from '../onboarding-copy.ts'
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
} satisfies Record<string, string>
/** Simplified Chinese dictionary. */
export const zh: LocaleDict = {
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
...SHARED,
'trigger': '设置',
'title': '设置',
@@ -34,10 +32,13 @@ export const zh: LocaleDict = {
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.zh.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.zh.continueLabel,
'welcome.error': '暂时无法保存确认状态,请重试。',
}
} satisfies Record<string, string>
/** English dictionary. */
export const en: LocaleDict = {
/** The settings namespace key union. */
export type SettingsKey = keyof typeof zh
/** English dictionary, checked complete against the zh key set. */
export const en = {
...SHARED,
'trigger': 'Settings',
'title': 'Settings',
@@ -54,4 +55,4 @@ export const en: LocaleDict = {
'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.en.feedbackEmphasis,
'welcome.continue': WELCOME_NOTICE_COPY.en.continueLabel,
'welcome.error': 'The acknowledgement could not be saved. Please try again.',
}
} satisfies Record<SettingsKey, string>
@@ -1,17 +1,17 @@
/** Ownerless-copy registrations: the four seats, the dictionaries, locale refresh, and HMR recovery. */
/** Ownerless-copy registrations: the four seats, the dictionaries, thunked labels, and HMR recovery. */
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
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'
import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx'
import type { WelcomeNoticeInjected } from '../src/client/WelcomeNotice.tsx'
import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts'
/** The four seats this plugin fills (slot name → expected component). */
/** The five seats this plugin fills (slot name → expected component). */
const SEATS = [
['settings.trigger', TriggerContent],
['settings.header', HeaderContent],
@@ -72,7 +72,7 @@ describe('ui-settings-general apply', () => {
expect(inject).toEqual(['slots', 'locale', 'connection'])
})
it('fills all four seats for declarations before or after apply', async () => {
it('fills all five seats for declarations before or after apply', async () => {
const before = await bench()
declare(before.slots)
await before.ctx.plugin({ inject: [...inject], apply }).await()
@@ -80,15 +80,16 @@ describe('ui-settings-general apply', () => {
expect(before.slots.entries(name)[0]!.component).toBe(component)
}
const entry = generalEntry(before.slots)!
expect(entry.options).toEqual({ id: 'general', order: 0, label: '通用设置' })
expect(entry.options).toMatchObject({ id: 'general', order: 0 })
// The nav label is a locale-following thunk; owners resolve at read time.
expect(resolveSlotLabel(entry.options.label)).toBe('通用设置')
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('权限')
const welcome = before.slots.entries('settings.onboarding')[0]!
expect(welcome.options).toEqual({ id: 'welcome-notice', order: -100 })
// 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('设置')
expect(welcome.options).toMatchObject({ id: 'welcome-notice', order: -100 })
// Copy rides the standard locale seat: every seat declares the namespace.
for (const [name] of SEATS) {
expect(before.slots.entries(name)[0]!.locale).toBe('settings')
}
const after = await bench()
await after.ctx.plugin({ inject: [...inject], apply }).await()
@@ -112,33 +113,26 @@ describe('ui-settings-general apply', () => {
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.
// The (ns, locale) seats are free again — the dictionary disposer 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 () => {
it('the nav label thunk follows the active locale without re-registration', 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.
// No ledger churn: freshness rides the thunk (and the renderer's locale
// subscription), not re-registration.
SEATS.forEach(([name], i) => {
expect(b.slots.getVersion(name)).toBeGreaterThan(zhVersions[i]!)
expect(b.slots.getVersion(name)).toBe(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)
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
b.locale.setLocale('zh')
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('通用设置')
})
it('refreshes loaded welcome state only for its settings namespace or a reconnect', async () => {
@@ -174,7 +168,7 @@ describe('ui-settings-general apply', () => {
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')
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
b.locale.setLocale('zh')
})
@@ -8,7 +8,9 @@ import { en } from '../src/client/locales.ts'
afterEach(cleanup)
const t = (key: string) => en[key] ?? key
// The seat's key domain is settings ∪ common; the stub answers from the
// package dictionary and falls back to the key like the real chain.
const t: GeneralSectionComponentProps['t'] = key => (en as Record<string, string>)[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
@@ -46,7 +46,7 @@ function mount(version?: string, mutateImpl: () => Promise<unknown> = () => Prom
useWorkspaces: unusedHook,
controller,
useSnapshot: bindSnapshotSelector(controller.store),
t: key => zh[key] ?? key,
t: key => key in zh ? zh[key as keyof typeof zh] : key,
}
return { ...render(<WelcomeNotice {...props} />), complete, controller, mutate }
}