feat(gui): add ask-user question composer
This commit is contained in:
@@ -295,11 +295,18 @@ describe('ChatView', () => {
|
||||
expect(lv.getByText('载入历史…')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('pending interactions render placeholder cards', () => {
|
||||
it('renders approval cards while questions stay in the composer', () => {
|
||||
const h = makeHarness({
|
||||
pending: [{ kind: 'approval', rpcId: 'r1' as never, approvalId: 'ap1', toolName: 'bash' }],
|
||||
pending: [
|
||||
{ kind: 'approval', rpcId: 'r1' as never, approvalId: 'ap1', toolName: 'bash' },
|
||||
{
|
||||
kind: 'question', rpcId: 'r2' as never,
|
||||
questions: [{ id: 'mode', question: 'Composer only?', options: [{ label: 'Yes' }] }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
expect(view.getByText(/等待审批/)).toBeTruthy()
|
||||
expect(view.queryByText('Composer only?')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// @vitest-environment jsdom
|
||||
// Branch tails the acceptance specs do not reach: ToolRow stopped-state dot,
|
||||
// PendingCard question arm, bash sample error pill, registry disposer
|
||||
// Bash sample error pill and registry disposer
|
||||
// idempotence re-entry, register.ts explicit bashSampleScope override, the
|
||||
// node-half empty apply, and AssistantMarkdown reasoning/unknown block arms.
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import type { ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { ToolViewRegistry } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ConversationService, Translate, ToolViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { apply as nodeApply } from '../src/index.ts'
|
||||
import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx'
|
||||
import { ToolRow } from '../src/client/chat/ToolRow.tsx'
|
||||
import { PendingCard } from '../src/client/chat/PendingCard.tsx'
|
||||
import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx'
|
||||
import { BashRow } from '../src/client/toolviews/bash-sample.tsx'
|
||||
import { registerChat } from '../src/client/chat/register.ts'
|
||||
@@ -34,13 +32,6 @@ describe('tails', () => {
|
||||
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('PendingCard renders the question arm with its count', () => {
|
||||
const view = render(
|
||||
<PendingCard item={{ kind: 'question', rpcId: 'r1' as RpcId, questions: [{}, {}] }} />,
|
||||
)
|
||||
expect(view.getByText(/等待回答(2 题)/)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
|
||||
const view = render(
|
||||
<AssistantMarkdown
|
||||
|
||||
@@ -10,11 +10,14 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ConversationSnapshot, SessionId, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { ConversationRoot, DetailsPanel, EmptyState } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { SelectionTarget, ViewEntry } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ConversationInjected, SelectionTarget, ViewEntry } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const SID = 's1' as SessionId
|
||||
const fallbackSlots: ConversationInjected['slots'] = {
|
||||
renderSlot: (_key, _props, opts) => opts?.fallback ?? null,
|
||||
}
|
||||
|
||||
function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
@@ -55,6 +58,7 @@ describe('ConversationRoot branches', () => {
|
||||
composer={{ useDraft: () => '', setDraft: vi.fn(), send: vi.fn(), stop: vi.fn() }}
|
||||
actions={{ openView: vi.fn(), open }}
|
||||
renderView={() => <div data-testid="view-body" />}
|
||||
slots={fallbackSlots}
|
||||
/>,
|
||||
)
|
||||
return { view, open }
|
||||
@@ -99,6 +103,7 @@ describe('ConversationRoot branches', () => {
|
||||
composer={{ useDraft: () => '', setDraft: vi.fn(), send: vi.fn(), stop: vi.fn() }}
|
||||
actions={{ openView: vi.fn(), open: vi.fn() }}
|
||||
renderView={(entry) => <div data-testid={`body-${entry.id}`} />}
|
||||
slots={fallbackSlots}
|
||||
/>,
|
||||
)
|
||||
expect(view.getByTestId('body-chat')).toBeTruthy()
|
||||
|
||||
@@ -11,11 +11,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { FC } from 'react'
|
||||
import { bindSnapshotSelector, createSnapshotStore } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { UseSession } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SessionId, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { PendingInteraction, SessionId, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
ConversationRoot, DetailsPanel, EmptyState,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { SelectionTarget, ViewEntry, ViewId } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ConversationInjected, SelectionTarget, ViewEntry, ViewId } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
|
||||
const sid = (s: string): SessionId => s as SessionId
|
||||
|
||||
@@ -28,11 +28,12 @@ interface FakeSnapshot {
|
||||
running: boolean
|
||||
removed: boolean
|
||||
promptError: { op: 'send' | 'stop'; error: { message: string; code: string } } | null
|
||||
pending: readonly PendingInteraction[]
|
||||
}
|
||||
|
||||
function fakeSession(init: Partial<FakeSnapshot> = {}) {
|
||||
const store = createSnapshotStore<FakeSnapshot>({
|
||||
nodes: [], runningCalls: [], running: false, removed: false, promptError: null, ...init,
|
||||
nodes: [], runningCalls: [], running: false, removed: false, promptError: null, pending: [], ...init,
|
||||
})
|
||||
return { store, useSession: bindSnapshotSelector(store) as unknown as UseSession }
|
||||
}
|
||||
@@ -67,8 +68,11 @@ describe('EmptyState', () => {
|
||||
})
|
||||
|
||||
describe('ConversationRoot', () => {
|
||||
function bench(views: ViewEntry[], active?: string) {
|
||||
const { useSession } = fakeSession({ nodes: [{ kind: 'user' }, { kind: 'user' }] })
|
||||
function bench(
|
||||
views: ViewEntry[], active?: string, init: Partial<FakeSnapshot> = {},
|
||||
renderSlot?: ConversationInjected['slots']['renderSlot'],
|
||||
) {
|
||||
const { useSession } = fakeSession({ nodes: [{ kind: 'user' }, { kind: 'user' }], ...init })
|
||||
const activeStore = createSnapshotStore<string | undefined>(active)
|
||||
const openView = vi.fn((v: string) => { activeStore.set(v) })
|
||||
const open = vi.fn()
|
||||
@@ -98,6 +102,7 @@ describe('ConversationRoot', () => {
|
||||
}}
|
||||
actions={{ openView: openView as (v: never) => void, open }}
|
||||
renderView={(entry) => { rendered.push(entry.id); return <div data-testid={`view-${entry.id}`} /> }}
|
||||
slots={{ renderSlot: renderSlot ?? ((_key, _props, opts) => opts?.fallback ?? null) } as ConversationInjected['slots']}
|
||||
/>)
|
||||
return { ui, openView, open, rendered, send, drafts }
|
||||
}
|
||||
@@ -133,6 +138,23 @@ describe('ConversationRoot', () => {
|
||||
fireEvent.keyDown(box, { key: 'Enter' })
|
||||
expect(send).toHaveBeenCalledWith('queue')
|
||||
})
|
||||
|
||||
it('dispatches a pending question to the composer slot instead of rendering InputBar', () => {
|
||||
const renderSlot = vi.fn(() => <div>question takeover</div>) as unknown as ConversationInjected['slots']['renderSlot']
|
||||
bench([view('chat', 'Chat')], undefined, {
|
||||
pending: [{
|
||||
kind: 'question', rpcId: 'rq' as never,
|
||||
questions: [{ id: 'mode', question: 'Choose?', options: [{ label: 'Fast' }] }],
|
||||
}],
|
||||
}, renderSlot)
|
||||
expect(screen.getByText('question takeover')).toBeTruthy()
|
||||
expect(screen.queryByPlaceholderText(/输入消息/)).toBeNull()
|
||||
expect(renderSlot).toHaveBeenCalledWith(
|
||||
'conversation.composer',
|
||||
expect.objectContaining({ interaction: expect.objectContaining({ rpcId: 'rq' }) }),
|
||||
expect.objectContaining({ entryKey: 'question' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DetailsPanel', () => {
|
||||
|
||||
Reference in New Issue
Block a user