Merge remote-tracking branch 'origin/master' into worktree/web-session-model-selector
# Conflicts: # apps/cli/package.json # apps/web/tests/smoke-real.e2e.ts # apps/web/tests/snapshots/fresh-round-trip/ui.expected.md # apps/web/tests/snapshots/seeded-history/ui.expected.md # docs/config-catalog.md # packages/client/connection/tests/fake-api.ts # packages/client/runtime/src/client/index.ts # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/runtime/src/client/sessions/session.ts # packages/client/runtime/tests/fake-api.ts # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-conversation/src/client/contract/slots.ts # packages/client/ui-conversation/src/client/index.ts # packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx # packages/client/ui-conversation/src/client/skeleton/InputBar.tsx # packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx # packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx # packages/client/ui-conversation/tests/chat-view.spec.tsx # packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx # packages/client/ui-conversation/tests/input-bar.spec.tsx # packages/client/ui-conversation/tests/skeleton.spec.tsx # packages/host/apiproxy/README.md # packages/host/apiproxy/src/api-proxy.ts # pnpm-lock.yaml # scripts/verify-package-readme-model-experience.ts # tsconfig.base.json
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Browser-plugin assembly: the selector occupies the conversation-declared
|
||||
* composer-control slot, injects only Session object actions, fails loud when
|
||||
* model-control slot, injects only Session object actions, fails loud when
|
||||
* the slot is undeclared, and unregisters with its plugin fiber.
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ async function bench(hasBinding = true) {
|
||||
slots.register({
|
||||
name: 'root',
|
||||
children: {
|
||||
'conversation.composer.control': { kind: 'single', scope: 'session' },
|
||||
'conversation.input.model': { kind: 'single', scope: 'session' },
|
||||
},
|
||||
} as never, (_props: { renderSlot?: unknown }) => null)
|
||||
const session = {
|
||||
@@ -47,13 +47,13 @@ describe('model-selector browser plugin', () => {
|
||||
ctx.provide('sessions', { binding: vi.fn() })
|
||||
ctx.provide('conversation', {})
|
||||
await expect(ctx.plugin({ inject: [...inject], apply }))
|
||||
.rejects.toThrow(/slot "conversation\.composer\.control" is not declared/)
|
||||
.rejects.toThrow(/slot "conversation\.input\.model" is not declared/)
|
||||
})
|
||||
|
||||
it('registers the singleton and injects Session-owned refresh/select actions', async () => {
|
||||
const { ctx, slots, session } = await bench()
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const entries = slots.entries('conversation.composer.control')
|
||||
const entries = slots.entries('conversation.input.model')
|
||||
expect(entries).toHaveLength(1)
|
||||
expect(entries[0]?.component).toBe(ModelSelector)
|
||||
const injected = (entries[0]?.inject as (sessionId: SessionId) => {
|
||||
@@ -75,7 +75,7 @@ describe('model-selector browser plugin', () => {
|
||||
it('fails loud when slot injection resolves no Session binding', async () => {
|
||||
const { ctx, slots } = await bench(false)
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
const entry = slots.entries('conversation.composer.control')[0]
|
||||
const entry = slots.entries('conversation.input.model')[0]
|
||||
expect(() => (entry?.inject as (sessionId: SessionId) => unknown)(SID))
|
||||
.toThrow(`ui-model-selector: session "${SID}" resolved no binding`)
|
||||
})
|
||||
@@ -84,8 +84,8 @@ describe('model-selector browser plugin', () => {
|
||||
const { ctx, slots } = await bench()
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
expect(slots.entries('conversation.composer.control')).toHaveLength(1)
|
||||
expect(slots.entries('conversation.input.model')).toHaveLength(1)
|
||||
await fiber.dispose()
|
||||
expect(slots.entries('conversation.composer.control')).toHaveLength(0)
|
||||
expect(slots.entries('conversation.input.model')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
ConversationSnapshot, ModelSelectionSnapshot,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ConversationSnapshot, ModelSelectionSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ModelTarget } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ModelSelectorProps } from '../src/client/contract.ts'
|
||||
import { ModelSelector } from '../src/client/ModelSelector.tsx'
|
||||
@@ -40,10 +38,9 @@ const ready: ModelSelectionSnapshot = {
|
||||
|
||||
function setup(
|
||||
selection: ModelSelectionSnapshot = ready,
|
||||
removed = false,
|
||||
intent: ConversationSnapshot['intent'] = null,
|
||||
locked = false,
|
||||
) {
|
||||
let current = { modelSelection: selection, removed, intent } as unknown as ConversationSnapshot
|
||||
let current = { modelSelection: selection } as unknown as ConversationSnapshot
|
||||
const useSession = ((selector: (snapshot: ConversationSnapshot) => unknown) =>
|
||||
selector(current)) as ModelSelectorProps['useSession']
|
||||
const refreshModels = vi.fn()
|
||||
@@ -51,11 +48,15 @@ function setup(
|
||||
const selectModel = vi.fn((_target: ModelTarget) => Promise.resolve(true))
|
||||
const props: ModelSelectorProps = {
|
||||
sessionId: 'selector-session' as never,
|
||||
locked,
|
||||
useSession,
|
||||
useSessions: ((selector: (snapshot: never) => unknown) =>
|
||||
selector({} as never)) as ModelSelectorProps['useSessions'],
|
||||
useWorkspaces: ((selector: (snapshot: never) => unknown) =>
|
||||
selector({} as never)) as ModelSelectorProps['useWorkspaces'],
|
||||
useInput: ((selector: (snapshot: never) => unknown) =>
|
||||
selector({} as never)) as ModelSelectorProps['useInput'],
|
||||
inputActions: {} as ModelSelectorProps['inputActions'],
|
||||
refreshModels,
|
||||
retryModelOperation,
|
||||
selectModel,
|
||||
@@ -68,14 +69,8 @@ function setup(
|
||||
selectModel,
|
||||
update(
|
||||
next: ModelSelectionSnapshot,
|
||||
nextRemoved = removed,
|
||||
nextIntent: ConversationSnapshot['intent'] = intent,
|
||||
) {
|
||||
current = {
|
||||
modelSelection: next,
|
||||
removed: nextRemoved,
|
||||
intent: nextIntent,
|
||||
} as unknown as ConversationSnapshot
|
||||
current = { modelSelection: next } as unknown as ConversationSnapshot
|
||||
view.rerender(<ModelSelector {...props} />)
|
||||
},
|
||||
}
|
||||
@@ -200,21 +195,8 @@ describe('model selector', () => {
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
})
|
||||
|
||||
it('disables the trigger only when the session is removed', () => {
|
||||
it('disables the trigger when the model-control seat is locked', () => {
|
||||
setup(ready, true)
|
||||
expect(trigger().disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('waits for a frontend Session Intent to publish before rendering or loading models', () => {
|
||||
const b = setup(ready, false, {
|
||||
target: { kind: 'workspace-intent' },
|
||||
phase: 'connecting',
|
||||
})
|
||||
expect(screen.queryByRole('button', { name: /选择模型,当前/ })).toBeNull()
|
||||
expect(b.refreshModels).not.toHaveBeenCalled()
|
||||
|
||||
b.update(ready, false, null)
|
||||
expect(trigger().textContent).toBe('DeepSeek-V4-Flash')
|
||||
expect(b.refreshModels).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user