|
|
@@ -9,7 +9,7 @@ import {
|
|
|
import type { SessionBehaviorOverrides } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
import {
|
|
|
apply, inject, type ComposerBarInjected, type ConversationInjected,
|
|
|
- type ConversationSessionInjected, type ViewTab,
|
|
|
+ type ConversationSessionHeaderInjected, type ConversationSessionInjected, type ViewTab,
|
|
|
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
|
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
|
|
@@ -51,7 +51,7 @@ async function bench() {
|
|
|
|
|
|
const feature = await runtime.mount({ inject: [...inject], apply })
|
|
|
runtime.renderRoot()
|
|
|
- const entryOf = (key: 'conversation' | 'conversation.session' | 'conversation.composer.bar') =>
|
|
|
+ const entryOf = (key: 'conversation' | 'conversation.session' | 'conversation.session.header' | 'conversation.composer.bar') =>
|
|
|
runtime.slots.entries(key)[0]!
|
|
|
const conversationApi = (id: SessionId) => {
|
|
|
const entry = entryOf('conversation.session')
|
|
|
@@ -66,6 +66,15 @@ async function bench() {
|
|
|
const entry = entryOf('conversation')
|
|
|
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ConversationInjected)(id)
|
|
|
}
|
|
|
+ const headerApi = (id: SessionId) => {
|
|
|
+ const entry = entryOf('conversation.session.header')
|
|
|
+ const instance = runtime.storeOf('conversation.session.header', id) as ConversationInstance
|
|
|
+ const injected = (entry.inject as unknown as (
|
|
|
+ sessionId: SessionId,
|
|
|
+ actions: ConversationActions,
|
|
|
+ ) => ConversationSessionHeaderInjected)(id, instance.actions)
|
|
|
+ return { instance, injected }
|
|
|
+ }
|
|
|
const composerApi = (id: SessionId | undefined) => {
|
|
|
const entry = entryOf('conversation.composer.bar')
|
|
|
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ComposerBarInjected)(id)
|
|
|
@@ -77,7 +86,7 @@ async function bench() {
|
|
|
const viewSource = (id: SessionId): ObservableSnapshot<readonly ViewTab[]> =>
|
|
|
conversationApi(id).injected.hooks.conversationViews
|
|
|
return {
|
|
|
- runtime, feature, slots: runtime.slots, entryOf, conversationApi, residentApi, composerApi,
|
|
|
+ runtime, feature, slots: runtime.slots, entryOf, conversationApi, headerApi, residentApi, composerApi,
|
|
|
inputApi, viewSource, sessionFake, connectWorkspace,
|
|
|
}
|
|
|
}
|
|
|
@@ -87,11 +96,79 @@ describe('Conversation inject API', () => {
|
|
|
const b = await bench()
|
|
|
const { injected } = b.conversationApi(ROOT)
|
|
|
expect(b.sessionFake.loadOlder).not.toHaveBeenCalled()
|
|
|
- expect(Object.keys(injected)).toEqual(['hooks', 'bindDraftMirror'])
|
|
|
+ expect(Object.keys(injected)).toEqual(['hooks', 'bindDraftMirror', 'openView'])
|
|
|
expect(b.viewSource(ROOT).getSnapshot()).toEqual([])
|
|
|
await b.runtime.dispose()
|
|
|
})
|
|
|
|
|
|
+ it('activates a target before committing an explicit View selection', async () => {
|
|
|
+ const b = await bench()
|
|
|
+ const binding = b.runtime.ctx.uiConversation.binding(ROOT)
|
|
|
+ const activate = vi.spyOn(binding, 'activate')
|
|
|
+ const removeChat = b.slots.register(
|
|
|
+ { name: 'conversation.view', id: 'chat', order: 0 },
|
|
|
+ (() => null) as never,
|
|
|
+ )
|
|
|
+ const removeTrajectory = b.slots.register(
|
|
|
+ { name: 'conversation.view', id: 'trajectory', order: 10 },
|
|
|
+ (() => null) as never,
|
|
|
+ )
|
|
|
+ await Promise.resolve()
|
|
|
+ activate.mockClear()
|
|
|
+
|
|
|
+ const body = b.conversationApi(ROOT)
|
|
|
+ body.injected.openView('trajectory', 'call-1')
|
|
|
+ expect(activate).toHaveBeenLastCalledWith('trajectory')
|
|
|
+ expect(body.instance.store.getSnapshot()).toMatchObject({
|
|
|
+ view: 'trajectory',
|
|
|
+ viewRequest: { view: 'trajectory', focus: 'call-1' },
|
|
|
+ })
|
|
|
+
|
|
|
+ const header = b.headerApi(ROOT)
|
|
|
+ header.injected.selectView('chat')
|
|
|
+ expect(activate).toHaveBeenLastCalledWith('chat')
|
|
|
+ expect(header.instance.store.getSnapshot().view).toBe('chat')
|
|
|
+
|
|
|
+ removeTrajectory()
|
|
|
+ removeChat()
|
|
|
+ await b.runtime.dispose()
|
|
|
+ })
|
|
|
+
|
|
|
+ it('restores the selected View when a cached Session becomes current', async () => {
|
|
|
+ const b = await bench()
|
|
|
+ const binding = b.runtime.ctx.uiConversation.binding(ROOT)
|
|
|
+ const activate = vi.spyOn(binding, 'activate')
|
|
|
+ const removeChat = b.slots.register(
|
|
|
+ { name: 'conversation.view', id: 'chat', order: 0 },
|
|
|
+ (() => null) as never,
|
|
|
+ )
|
|
|
+ let removeCustom: (() => void) | undefined
|
|
|
+ try {
|
|
|
+ await b.runtime.flush()
|
|
|
+ localStorage.setItem(`dsh.conversation.${ROOT}`, JSON.stringify({
|
|
|
+ draft: '', view: 'custom', viewRequest: null,
|
|
|
+ }))
|
|
|
+
|
|
|
+ b.runtime.ctx.uiSession.adapter.resolve(ROOT)
|
|
|
+ expect(activate).toHaveBeenLastCalledWith('chat')
|
|
|
+ activate.mockClear()
|
|
|
+
|
|
|
+ removeCustom = b.slots.register(
|
|
|
+ { name: 'conversation.view', id: 'custom', order: 10 },
|
|
|
+ (() => null) as never,
|
|
|
+ )
|
|
|
+ await b.runtime.flush()
|
|
|
+ expect(activate).not.toHaveBeenCalled()
|
|
|
+
|
|
|
+ await b.runtime.sessions.setCurrent(ROOT)
|
|
|
+ expect(activate).toHaveBeenLastCalledWith('custom')
|
|
|
+ } finally {
|
|
|
+ removeCustom?.()
|
|
|
+ removeChat()
|
|
|
+ await b.runtime.dispose()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
it('submits through the provided input machine and mirrors accepted draft edits', async () => {
|
|
|
const b = await bench()
|
|
|
const { injected } = b.conversationApi(ROOT)
|