fix: docs
This commit is contained in:
imccyu
2026-07-30 15:56:41 +08:00
parent 085383b145
commit 1e10966ef6
160 changed files with 2521 additions and 1135 deletions

View File

@@ -25,6 +25,7 @@
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slash",
"@deepseek-ai/dsh-client-ui-conversation"
],
@@ -40,6 +41,7 @@
},
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-locale": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
@@ -51,7 +53,9 @@
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",

View File

@@ -13,6 +13,7 @@ import { useEffect, useRef } from 'react'
import { useSyncExternalStore } from 'react'
import clsx from 'clsx'
import { IconCheckOutline16, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import { filterOptions } from './popup.ts'
import type { PopupSelectController } from './popup.ts'
import css from './PopupSelectView.module.css'
@@ -26,12 +27,15 @@ export interface PopupSelectInjected {
popup: PopupSelectController
}
/** Full shell props: injected face + the locale seat. */
export type PopupSelectViewProps = PopupSelectInjected & PropsLocale<'command'>
/**
* Render the popupSelect shell overlay entry.
* @param props - injected face: the session's shell controller.
* @param props - injected face: the session's shell controller; `t` rides the standard locale seat.
* @returns the select card while open; null while closed.
*/
export function PopupSelectView({ popup }: PopupSelectInjected) {
export function PopupSelectView({ popup, t }: PopupSelectViewProps) {
const state = useSyncExternalStore(
fn => popup.state.subscribe(fn),
() => popup.state.getSnapshot(),
@@ -103,15 +107,15 @@ export function PopupSelectView({ popup }: PopupSelectInjected) {
ref={cardRef}
className={css.card}
style={{ maxHeight }}
aria-label={`/${String(state.command)} options`}
aria-label={t('overlay.aria', { command: String(state.command) })}
onKeyDown={onKeyDown}
>
<input
ref={searchRef}
className={css.search}
type="text"
placeholder="Search…"
aria-label="Filter options"
placeholder={t('search.placeholder')}
aria-label={t('search.aria')}
value={state.search}
readOnly={state.submitting}
onChange={(ev) => { popup.setSearch(ev.currentTarget.value) }}
@@ -120,15 +124,15 @@ export function PopupSelectView({ popup }: PopupSelectInjected) {
<div className={css.error} role="alert">
<span className={css.errorText}>{state.error}</span>
{state.status === 'failed' && (
<button type="button" className={css.retry} onClick={() => { popup.retry() }}>Retry</button>
<button type="button" className={css.retry} onClick={() => { popup.retry() }}>{t('retry')}</button>
)}
</div>
)}
{state.status === 'pending' && <div className={css.status}>Loading options</div>}
{state.submitting && <div className={css.status}>Applying</div>}
{state.status === 'ready' && rows.length === 0 && <div className={css.status}>No options</div>}
{state.status === 'pending' && <div className={css.status}>{t('status.loading')}</div>}
{state.submitting && <div className={css.status}>{t('status.applying')}</div>}
{state.status === 'ready' && rows.length === 0 && <div className={css.status}>{t('status.empty')}</div>}
{state.status === 'ready' && (
<div role="listbox" aria-label={`/${String(state.command)} matches`} className={css.viewport}>
<div role="listbox" aria-label={t('listbox.aria', { command: String(state.command) })} className={css.viewport}>
{rows.map((option, index) => (
<div
key={option.id}

View File

@@ -10,19 +10,23 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
// key's owner) into this program so the overlay registration below typechecks
// against the real declaration — no runtime edge to ui-conversation.
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
import type {} from '@deepseek-ai/dsh-client-locale/client'
import { CommandService } from './service.ts'
import type { PopupSelectInjected } from './PopupSelectView.tsx'
import { PopupSelectView } from './PopupSelectView.tsx'
import { en, zh, type CommandKey } from './locales.ts'
export { CommandService } from './service.ts'
export { CommandDirectory } from './directory.ts'
export type { CommandDescriptor, DirectoryStatus } from './directory.ts'
export { filterOptions, PopupSelectController } from './popup.ts'
export type { PopupSelectDeps, PopupSpec, PopupState, TokenSegment } from './popup.ts'
export type { PopupSelectInjected } from './PopupSelectView.tsx'
export type { PopupSelectInjected, PopupSelectViewProps } from './PopupSelectView.tsx'
export type {
CommandContribution, CommandDecoration, CommandServiceContract, CommandUiSpec, SelectOption,
} from './contract.ts'
export type { CommandKey } from './locales.ts'
declare module 'cordis' {
interface Context {
@@ -30,8 +34,18 @@ declare module 'cordis' {
}
}
/** Required services: the '/' source registry plus the scope + wire faces the service reads. */
export const inject = ['slash', 'sessions', 'connection']
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** The popupSelect shell's copy. */
command: CommandKey
}
}
/** Dictionary namespace owned by this plugin. */
const NS = 'command'
/** Required services: the '/' source registry plus the scope + wire faces the service reads, and the copy's locale registry. */
export const inject = ['slash', 'sessions', 'connection', 'locale']
/**
* Client plugin body: mount the service, then register the popupSelect shell
@@ -39,6 +53,7 @@ export const inject = ['slash', 'sessions', 'connection']
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-command: dictionaries')
ctx.plugin(CommandService)
// Conditional mount, same seam as ui-slash's MenuView registration:
// 'conversation.input.overlay' is declared by the conversation composer
@@ -51,6 +66,7 @@ export function apply(ctx: ClientContext): void {
name: 'conversation.input.overlay',
id: 'command-popup',
order: 1,
locale: NS,
inject: (sessionId): PopupSelectInjected => {
const actx = sessions.scope(sessionId)
if (actx === undefined) throw new Error(`ui-command: session "${String(sessionId)}" resolved no scope`)

View File

@@ -0,0 +1,26 @@
/** `command` namespace dictionaries (the popupSelect shell's copy). */
/** Simplified Chinese dictionary (the key-set source of truth). */
export const zh = {
'search.placeholder': '搜索…',
'search.aria': '筛选选项',
'status.loading': '正在加载选项…',
'status.applying': '正在应用…',
'status.empty': '无选项',
'overlay.aria': '/{command} 选项',
'listbox.aria': '/{command} 匹配项',
} satisfies Record<string, string>
/** The command namespace key union. */
export type CommandKey = keyof typeof zh
/** English dictionary, checked complete against the zh key set. */
export const en = {
'search.placeholder': 'Search…',
'search.aria': 'Filter options',
'status.loading': 'Loading options…',
'status.applying': 'Applying…',
'status.empty': 'No options',
'overlay.aria': '/{command} options',
'listbox.aria': '/{command} matches',
} satisfies Record<CommandKey, string>

View File

@@ -13,6 +13,7 @@ import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
import type { CommandServiceContract } from '../src/client/contract.ts'
import type { PopupSelectInjected } from '../src/client/PopupSelectView.tsx'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { apply, CommandService, inject } from '../src/client/index.ts'
const sid = (k: string): SessionId => k as SessionId
@@ -41,6 +42,7 @@ async function bench() {
},
})
ctx.provide('conversation', {})
ctx.provide('locale', new LocaleService(ctx))
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
const mint = (key: string) => {
@@ -53,7 +55,7 @@ async function bench() {
describe('apply', () => {
it('declares the services it binds', () => {
expect(inject).toEqual(['slash', 'sessions', 'connection'])
expect(inject).toEqual(['slash', 'sessions', 'connection', 'locale'])
})
it('mounts ctx.command, registers the source and the overlay entry, and folds up on disposal', async () => {

View File

@@ -14,6 +14,12 @@ import type { SelectOption } from '../src/client/contract.ts'
import type { PopupSpec, TokenSegment } from '../src/client/popup.ts'
import { PopupSelectController } from '../src/client/popup.ts'
import { PopupSelectView } from '../src/client/PopupSelectView.tsx'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { zh } from '../src/client/locales.ts'
// The framework-injected t seat, stubbed over the zh dictionaries (the default locale).
const t: Parameters<typeof PopupSelectView>[0]['t'] = makeTranslate(zh, commonZh)
// jsdom has no scrollIntoView; the view calls it on the highlighted row.
const scrollIntoView = vi.fn()
@@ -47,12 +53,12 @@ async function mountOpen(overrides: Partial<PopupSpec<string>> = {}, consumeResu
const consume = vi.fn((_segment: TokenSegment) => consumeResult)
const focusComposer = vi.fn()
const popup = new PopupSelectController<string>({ consume, focusComposer })
const view = render(<PopupSelectView popup={popup} />)
const view = render(<PopupSelectView popup={popup} t={t} />)
await act(async () => {
popup.open('theme', spec(overrides), 'ctx-A', SEGMENT)
await Promise.resolve()
})
return { popup, view, consume, focusComposer, search: screen.getByRole('textbox', { name: 'Filter options' }) }
return { popup, view, consume, focusComposer, search: screen.getByRole('textbox', { name: '筛选选项' }) }
}
function rowLabels(): string[] {
@@ -62,13 +68,13 @@ function rowLabels(): string[] {
describe('PopupSelectView', () => {
it('renders null while closed, opens with focus in the search input', async () => {
const popup = new PopupSelectController<string>({ consume: () => true, focusComposer: () => {} })
const view = render(<PopupSelectView popup={popup} />)
const view = render(<PopupSelectView popup={popup} t={t} />)
expect(view.container.childElementCount).toBe(0)
await act(async () => {
popup.open('theme', spec(), 'ctx-A', SEGMENT)
await Promise.resolve()
})
const search = screen.getByRole('textbox', { name: 'Filter options' })
const search = screen.getByRole('textbox', { name: '筛选选项' })
expect(document.activeElement).toBe(search)
expect(rowLabels()).toEqual(['Dark', 'Light', 'Sepia'])
})
@@ -82,7 +88,7 @@ describe('PopupSelectView', () => {
expect(options).toHaveBeenCalledTimes(1)
act(() => { fireEvent.change(search, { target: { value: 'zzz' } }) })
expect(screen.queryByRole('option')).toBeNull()
expect(screen.queryByText('No options')).not.toBeNull()
expect(screen.queryByText('无选项')).not.toBeNull()
})
it('ArrowUp/Down move the filtered highlight; ArrowLeft/Right are left to the native caret', async () => {
@@ -110,13 +116,13 @@ describe('PopupSelectView', () => {
it('caps the card height at the design maximum when the composer sits low enough', async () => {
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ bottom: 800 } as DOMRect)
await mountOpen()
expect(screen.getByLabelText('/theme options').style.maxHeight).toBe('320px')
expect(screen.getByLabelText('/theme 选项').style.maxHeight).toBe('320px')
})
it('clamps the card height to the space above the composer minus the safe margin', async () => {
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ bottom: 200 } as DOMRect)
await mountOpen()
expect(screen.getByLabelText('/theme options').style.maxHeight).toBe('188px')
expect(screen.getByLabelText('/theme 选项').style.maxHeight).toBe('188px')
})
it('Enter selects the highlighted row: onSelect, consume, close, focusComposer', async () => {
@@ -148,7 +154,7 @@ describe('PopupSelectView', () => {
const onSelect = vi.fn(() => new Promise<void>((resolve) => { release = resolve }))
const { search, consume } = await mountOpen({ onSelect })
await act(async () => { fireEvent.keyDown(search, { key: 'Enter' }) })
expect(screen.queryByText('Applying…')).not.toBeNull()
expect(screen.queryByText('正在应用…')).not.toBeNull()
expect((search as HTMLInputElement).readOnly).toBe(true)
await act(async () => {
fireEvent.keyDown(search, { key: 'Enter' })
@@ -162,7 +168,7 @@ describe('PopupSelectView', () => {
expect(consume).toHaveBeenCalledTimes(1)
})
it('a failed options load shows the error with a Retry button that reloads', async () => {
it('a failed options load shows the error with a retry button that reloads', async () => {
let attempts = 0
await mountOpen({
options: () => {
@@ -172,7 +178,7 @@ describe('PopupSelectView', () => {
})
expect(screen.getByRole('alert').textContent).toContain('directory down')
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
fireEvent.click(screen.getByRole('button', { name: '重试' }))
await Promise.resolve()
})
expect(attempts).toBe(2)
@@ -183,7 +189,7 @@ describe('PopupSelectView', () => {
const { search, consume } = await mountOpen({ onSelect: () => Promise.reject(new Error('host rejected')) })
await act(async () => { fireEvent.keyDown(search, { key: 'Enter' }) })
expect(screen.getByRole('alert').textContent).toContain('host rejected')
expect(screen.queryByRole('button', { name: 'Retry' })).toBeNull()
expect(screen.queryByRole('button', { name: '重试' })).toBeNull()
expect(consume).not.toHaveBeenCalled()
expect(screen.getAllByRole('option').length).toBe(3)
})

View File

@@ -14,6 +14,9 @@
{
"path": "../connection"
},
{
"path": "../locale"
},
{
"path": "../runtime"
},