Merge remote-tracking branch 'origin/master' into worktree/web-ask-user-question

This commit is contained in:
imccyu
2026-07-23 14:05:08 +08:00
190 changed files with 6061 additions and 4213 deletions

View File

@@ -38,6 +38,7 @@
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
"clsx": "^2.0.0",
"react": "^18.2.0"

View File

@@ -1,7 +1,6 @@
import { useState, type KeyboardEvent } from 'react'
import clsx from 'clsx'
import type { QuestionResponsePayload } from '@deepseek-ai/dsh-client-connection/client'
import type { PendingInteraction } from '@deepseek-ai/dsh-client-runtime/client'
import {
Button, IconCheckOutline16, IconChevronLeftOutline14, IconChevronRightOutline14,
IconCloseOutline16, IconEditOutline16,
@@ -9,7 +8,7 @@ import {
import type { QuestionComposerOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
import css from './QuestionComposer.module.css'
type QuestionInteraction = Extract<PendingInteraction, { kind: 'question' }>
type QuestionInteraction = QuestionComposerOwnerProps['interaction']
type Answer = QuestionResponsePayload['answer']
interface DraftAnswer {
@@ -27,7 +26,9 @@ export interface QuestionComposerInjected {
}
}
/** Full question-composer props. */
/** Consumed question-composer props: the slot's owner share & the injected
* share. A strict subset of the composed props the register site proves
* (the framework session/global standard kit goes unconsumed here). */
export type QuestionComposerProps = QuestionComposerOwnerProps & QuestionComposerInjected
/**

View File

@@ -1,9 +1,11 @@
/**
* Web question plugin, browser half: registers a composer replacement for
* pending ask_user_question requests.
* pending ask_user_question requests into the conversation-declared keyed
* `conversation.composer` slot (single register API — the slot exists because
* the conversation entry's children declaration created it).
*/
import type { Context } from 'cordis'
import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionId, SessionsService, SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { QuestionComposer, type QuestionComposerInjected } from './QuestionComposer.tsx'
export { QuestionComposer, parseRecommendedLabel } from './QuestionComposer.tsx'
@@ -17,15 +19,16 @@ export const inject = ['slots', 'sessions']
* @param ctx - Browser plugin context carrying slots and sessions.
*/
export function apply(ctx: Context): void {
const slots = ctx.get('slots')
const slots = ctx.get('slots') as SlotsService | undefined
const sessions = ctx.get('sessions') as SessionsService | undefined
if (slots === undefined || sessions === undefined) {
throw new Error('ui-question: slots and sessions services are required')
}
slots.register<'conversation.composer', QuestionComposerInjected>('conversation.composer', QuestionComposer, {
slots.register({
name: 'conversation.composer',
key: 'question',
inject(binding): QuestionComposerInjected {
const session = sessions.manager.get(binding.sessionId as SessionId)
inject: (sessionId: SessionId): QuestionComposerInjected => {
const session = sessions.manager.get(sessionId)
return {
actions: {
async answer(interaction, answer) {
@@ -43,5 +46,5 @@ export function apply(ctx: Context): void {
},
}
},
})
}, QuestionComposer)
}

View File

@@ -14,6 +14,15 @@ function interaction(): QuestionInteraction {
}
}
/** Declare the conversation-owned composer slot the way production does: a
* parent entry's children table (register is the single declaration API). */
function declareComposerSlot(slots: SlotsService): void {
slots.register({
name: 'root',
children: { 'conversation.composer': { kind: 'keyed', scope: 'session' } },
} as never, (() => null) as never)
}
describe('ui-question browser plugin', () => {
it('declares its services and fails loud without them', () => {
expect(inject).toEqual(['slots', 'sessions'])
@@ -33,17 +42,19 @@ describe('ui-question browser plugin', () => {
manager: { get: vi.fn(() => ({ answerQuestion, cancelQuestion })) },
})
const slots = ctx.get('slots') as SlotsService
slots.define('conversation.composer', { kind: 'keyed', scope: 'session' })
declareComposerSlot(slots)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
const entry = slots.entries('conversation.composer')[0] as unknown as {
options: { inject(binding: { sessionId: SessionId }): { actions: {
options: { key: string }
inject(sessionId: SessionId): { actions: {
answer: (item: QuestionInteraction, answer: { answers: { id: string; selected: string[] }[] }) => Promise<void>
cancel: (item: QuestionInteraction) => Promise<void>
} } }
} }
}
const actions = entry.options.inject({ sessionId: 'session-1' as SessionId }).actions
expect(entry.options.key).toBe('question')
const actions = entry.inject('session-1' as SessionId).actions
const item = interaction()
const answer = { answers: [{ id: 'mode', selected: ['Fast'] }] }

View File

@@ -1,15 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"extends": "../../../tsconfig.base.client.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types",
"jsx": "react-jsx",
"lib": [
"ES2024",
"DOM",
"DOM.Iterable"
],
"types": []
"outDir": "lib/types"
},
"include": [
"src"
@@ -18,6 +11,9 @@
{
"path": "../../../vendor/cordis"
},
{
"path": "../connection"
},
{
"path": "../runtime"
},
@@ -30,9 +26,6 @@
{
"path": "../ui-slots"
},
{
"path": "../web-react"
},
{
"path": "../../ui/tool-ask-user"
},