feat(web): configure DeepSeek during onboarding

This commit is contained in:
Yichen Jiang
2026-07-30 12:41:09 +08:00
parent 65bd54f8b4
commit 9182db00ef
24 changed files with 1051 additions and 53 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-client-ui-settings",
"description": "Settings shell plugin: sidebar trigger + modal panel occupying sidebar.settings; declares the settings.section list slot",
"description": "Settings shell plugin: sidebar trigger, modal panel, feature sections, and root-scoped onboarding overlays",
"version": "0.0.1",
"private": true,
"type": "module",

View File

@@ -22,6 +22,8 @@ function navIcon(id: string) {
type PanelProps = {
rows: readonly SettingsSectionRow[]
renderSlot: SettingsRootComponentProps['renderSlot']
activeId: string | undefined
onSelect: (id: string) => void
onClose: () => void
}
@@ -30,10 +32,9 @@ type PanelProps = {
* header button, a mask click, and document-level Escape (mounted only while
* open, so the listener lifetime is the panel's).
*/
function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
// Local selection; entries can unmount underneath it, so the render-time
function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelProps) {
// Entries can unmount underneath the requested id, so the render-time
// projection falls back to the first row when the id is gone.
const [activeId, setActiveId] = useState<string | undefined>(undefined)
const active = rows.find(r => r.id === activeId)?.id ?? rows[0]?.id
const titleId = useId()
@@ -62,7 +63,7 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
type="button"
className={clsx(css.navCell, row.id === active && css.active)}
aria-current={row.id === active ? 'true' : undefined}
onClick={() => { setActiveId(row.id) }}
onClick={() => { onSelect(row.id) }}
>
{navIcon(row.id)}
<span className={css.navLabel}>{row.label}</span>
@@ -92,14 +93,25 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
* @returns the settings shell element tree.
*/
export function SettingsRoot(props: SettingsRootComponentProps) {
const { wide, useSections, renderSlot } = props
const { wide, useSections, useSessions, renderSlot } = props
const [open, setOpen] = useState(false)
const close = useCallback(() => { setOpen(false) }, [])
const [activeId, setActiveId] = useState<string | undefined>(undefined)
const close = useCallback(() => {
setOpen(false)
setActiveId(undefined)
}, [])
const openSection = useCallback((id: string) => {
setActiveId(id)
setOpen(true)
}, [])
// The ledger tick keeps the nav rows fresh: registrants re-register with
// freshly localized text on locale change, and the trigger/header/close
// seats re-render through their own outlets' subscriptions.
const rows = useSections(s => s)
const onboardingActive = useSessions(state =>
state.phase === 'ready'
&& (state.current === undefined || state.byId[state.current]?.blank === true))
return (
<>
@@ -112,7 +124,16 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
>
{renderSlot('settings.trigger', { wide })}
</button>
{open && <SettingsPanel rows={rows} renderSlot={renderSlot} onClose={close} />}
{open && (
<SettingsPanel
rows={rows}
renderSlot={renderSlot}
activeId={activeId}
onSelect={setActiveId}
onClose={close}
/>
)}
{renderSlot('settings.onboarding', { active: onboardingActive, openSection })}
</>
)
}

View File

@@ -47,6 +47,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* item registrant; the shell neither declares nor renders it.)
*/
'settings.section': { kind: 'list'; scope: 'root'; owner: SettingsSectionOwnerProps }
/**
* Root-scoped onboarding overlays contributed by settings features. The
* shell supplies whether the current navigation state is the empty Hero
* and a private callback that opens one settings section; registrants own
* readiness, copy, and dialog behavior.
*/
'settings.onboarding': { kind: 'list'; scope: 'root'; owner: SettingsOnboardingOwnerProps }
}
}
@@ -72,6 +79,14 @@ export interface SettingsSectionOwnerProps {
children?: never
}
/** Owner share of a settings-backed onboarding overlay. */
export interface SettingsOnboardingOwnerProps {
/** Whether the current UI is in its empty Hero/onboarding state. */
active: boolean
/** Open the settings panel directly on one registered section. */
openSection: (id: string) => void
}
/** One nav row projected from a settings.section registration's options. */
export interface SettingsSectionRow {
id: string
@@ -99,5 +114,7 @@ export type SettingsRootInjected = {
*/
export type SettingsRootComponentProps =
PropsRuntime<'sidebar.settings'>
& PropsRenderSlots<'settings.trigger' | 'settings.header' | 'settings.close' | 'settings.section'>
& PropsRenderSlots<
'settings.trigger' | 'settings.header' | 'settings.close' | 'settings.section' | 'settings.onboarding'
>
& InjectFace<SettingsRootInjected>

View File

@@ -1,12 +1,11 @@
/**
* Settings shell plugin, browser half. A pure composition face: occupies the
* sidebar-owned `sidebar.settings` hole with the trigger chrome + modal
* panel, declares the `settings.trigger` / `settings.header` /
* `settings.section` slots, and projects the section ledger into the panel
* navigation. The shell ships no copy and reads no locale state — all text
* arrives from registrants (ui-settings-general owns the chrome and General
* content; features own their rows and sections). Export discipline:
* packages/client/AGENTS.md.
* panel, declares its chrome, section, and onboarding slots, and projects the
* section ledger into panel navigation. The shell ships no copy and reads no
* locale state — all text arrives from registrants (ui-settings-general owns
* the chrome and General content; features own their rows, sections, and
* onboarding overlays). 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'
@@ -15,7 +14,7 @@ import { SettingsRoot } from './SettingsRoot.tsx'
export type {
SettingsHeaderOwnerProps, SettingsRootComponentProps, SettingsRootInjected,
SettingsSectionOwnerProps, SettingsSectionRow, SettingsTriggerOwnerProps,
SettingsOnboardingOwnerProps, SettingsSectionOwnerProps, SettingsSectionRow, SettingsTriggerOwnerProps,
} from './contract/slots.ts'
/**
@@ -67,6 +66,7 @@ export function apply(ctx: ClientContext): void {
'settings.header': { kind: 'single', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
'settings.onboarding': { kind: 'list', scope: 'root' },
},
inject: injected,
}, SettingsRoot))

View File

@@ -24,12 +24,13 @@ function injectedOf(slots: SlotsService): SettingsRootInjected {
return (entry.inject as () => SettingsRootInjected)()
}
/** The shell's four child declarations (chrome seats + the section list). */
/** The shell's five child declarations (chrome, sections, and onboarding overlays). */
const CHILD_SPECS = {
'settings.trigger': { kind: 'single', scope: 'root' },
'settings.header': { kind: 'single', scope: 'root' },
'settings.close': { kind: 'single', scope: 'root' },
'settings.section': { kind: 'list', scope: 'root' },
'settings.onboarding': { kind: 'list', scope: 'root' },
} as const
describe('ui-settings apply', () => {
@@ -37,7 +38,7 @@ describe('ui-settings apply', () => {
expect(inject).toEqual(['slots'])
})
it('registers the shell and declares the four child slots, before or after the declaration', async () => {
it('registers the shell and declares the five child slots, before or after the declaration', async () => {
const before = await bench()
declare(before.slots)
await before.ctx.plugin({ inject: [...inject], apply }).await()
@@ -100,7 +101,7 @@ describe('ui-settings apply', () => {
}
})
it('unregisters the shell and collapses all four child slots on teardown', async () => {
it('unregisters the shell and collapses all five child slots on teardown', async () => {
const b = await bench()
declare(b.slots)
const fiber = b.ctx.plugin({ inject: [...inject], apply })

View File

@@ -18,11 +18,12 @@ const SEAT_CONTENT: Record<string, string> = {
function mount({
wide = true,
onboardingActive = true,
rows = [
{ id: 'general', order: 0, label: 'General' },
{ id: 'models', order: 10, label: 'Models' },
],
}: { wide?: boolean; rows?: Row[] } = {}) {
}: { wide?: boolean; onboardingActive?: boolean; rows?: Row[] } = {}) {
// Mutable row source standing in for the bound useSections hook; bump()
// plays a ledger change through the same observable contract.
let current = rows
@@ -33,10 +34,16 @@ function mount({
return SEAT_CONTENT[key]
}) as SettingsRootComponentProps['renderSlot'],
)
// Global standard kit stubs: the shell consumes neither hook.
const useSessions = ((select: (state: unknown) => unknown) => select(onboardingActive
? { phase: 'ready', current: undefined, byId: {} }
: {
phase: 'ready',
current: 'active-session',
byId: { 'active-session': { blank: false } },
})) as never
const unusedHook = (() => { throw new Error('unused by SettingsRoot') }) as never
const props: SettingsRootComponentProps = {
useSessions: unusedHook,
useSessions,
useWorkspaces: unusedHook,
wide,
useSections: (select) => {
@@ -157,6 +164,22 @@ describe('SettingsPanel navigation', () => {
expect(screen.queryByTestId('section-general')).toBeNull()
})
it('hands Hero readiness and a direct section opener to onboarding registrants', () => {
const { renderSlot } = mount()
const onboardingCall = renderSlot.mock.calls.find(call => call[0] === 'settings.onboarding')
expect(onboardingCall?.[1]).toMatchObject({ active: true })
act(() => {
(onboardingCall?.[1] as { openSection: (id: string) => void }).openSection('models')
})
expect(screen.getByRole('dialog')).toBeTruthy()
expect(screen.getByTestId('section-models')).toBeTruthy()
cleanup()
const active = mount({ onboardingActive: false }).renderSlot.mock.calls
.find(call => call[0] === 'settings.onboarding')
expect(active?.[1]).toMatchObject({ active: false })
})
it('falls back to the first row when the active entry unregisters', () => {
const { bump } = mount()
openPanel()