|
@@ -1,5 +1,6 @@
|
|
|
/**
|
|
/**
|
|
|
- * ui-skill browser half: source registration (duplicate-name proof) +
|
|
|
|
|
|
|
+ * ui-skill browser half: source and keyed toolview registration +
|
|
|
|
|
+ * locale dictionaries + source duplicate-name proof +
|
|
|
* fiber-teardown removal (HMR safety) against the real SlashService, then
|
|
* fiber-teardown removal (HMR safety) against the real SlashService, then
|
|
|
* the source behavior contract driven directly on the captured source with
|
|
* the source behavior contract driven directly on the captured source with
|
|
|
* real ClientSessionContext projections — sessionId addressing, the
|
|
* real ClientSessionContext projections — sessionId addressing, the
|
|
@@ -13,9 +14,11 @@
|
|
|
import { Context } from 'cordis'
|
|
import { Context } from 'cordis'
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
|
|
+import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
|
|
import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
|
|
|
import type { ClientSessionContext, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
|
import type { ClientSessionContext, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
|
|
import { apply, inject } from '../src/client/index.ts'
|
|
import { apply, inject } from '../src/client/index.ts'
|
|
|
|
|
+import { SkillRow as SkillToolRow } from '../src/client/SkillRow.tsx'
|
|
|
|
|
|
|
|
type SkillRow = { name: string; description: string; whenToUse?: string }
|
|
type SkillRow = { name: string; description: string; whenToUse?: string }
|
|
|
type ListResult =
|
|
type ListResult =
|
|
@@ -23,6 +26,33 @@ type ListResult =
|
|
|
| { ok: false; error: { code: string; message: string; details: object } }
|
|
| { ok: false; error: { code: string; message: string; details: object } }
|
|
|
type ListFn = (payload: object, signal?: AbortSignal) => Promise<{ result: ListResult }>
|
|
type ListFn = (payload: object, signal?: AbortSignal) => Promise<{ result: ListResult }>
|
|
|
|
|
|
|
|
|
|
+interface PresentationCapture {
|
|
|
|
|
+ slots: SlotsService
|
|
|
|
|
+ dictionaries: Array<{ namespace: string; dictionaries: unknown }>
|
|
|
|
|
+ localeDisposed: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Provide the presentation registries and capture the plugin's registrations. */
|
|
|
|
|
+function providePresentation(ctx: Context): PresentationCapture {
|
|
|
|
|
+ const slots = new SlotsService(ctx)
|
|
|
|
|
+ slots.register({
|
|
|
|
|
+ name: 'root',
|
|
|
|
|
+ children: { 'conversation.chat.toolview': { kind: 'keyed', scope: 'session' } },
|
|
|
|
|
+ } as never, () => null)
|
|
|
|
|
+ const capture: PresentationCapture = {
|
|
|
|
|
+ slots,
|
|
|
|
|
+ dictionaries: [],
|
|
|
|
|
+ localeDisposed: false,
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.provide('locale', {
|
|
|
|
|
+ register(namespace: string, dictionaries: unknown) {
|
|
|
|
|
+ capture.dictionaries.push({ namespace, dictionaries })
|
|
|
|
|
+ return () => { capture.localeDisposed = true }
|
|
|
|
|
+ },
|
|
|
|
|
+ })
|
|
|
|
|
+ return capture
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** Boot the plugin over fake slash/connection faces; returns the captured source and its ctx. */
|
|
/** Boot the plugin over fake slash/connection faces; returns the captured source and its ctx. */
|
|
|
async function bench(list: ListFn, addressed?: SessionId) {
|
|
async function bench(list: ListFn, addressed?: SessionId) {
|
|
|
const ctx = new Context()
|
|
const ctx = new Context()
|
|
@@ -34,6 +64,7 @@ async function bench(list: ListFn, addressed?: SessionId) {
|
|
|
? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const }
|
|
? { parentSessionId: sid('parent'), childSessionId: id, mode: 'continuable' as const }
|
|
|
: undefined,
|
|
: undefined,
|
|
|
})
|
|
})
|
|
|
|
|
+ providePresentation(ctx)
|
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
return { ctx, source: captured! }
|
|
return { ctx, source: captured! }
|
|
|
}
|
|
}
|
|
@@ -65,7 +96,36 @@ const req = (query: string, signal?: AbortSignal) =>
|
|
|
|
|
|
|
|
describe('apply', () => {
|
|
describe('apply', () => {
|
|
|
it('declares the services it binds', () => {
|
|
it('declares the services it binds', () => {
|
|
|
- expect(inject).toEqual(['slash', 'connection', 'sessions'])
|
|
|
|
|
|
|
+ expect(inject).toEqual(['slash', 'connection', 'sessions', 'slots', 'locale'])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('registers the dedicated skill row and its locale dictionaries', async () => {
|
|
|
|
|
+ const ctx = new Context()
|
|
|
|
|
+ ctx.provide('slash', { registerSource: () => () => {} })
|
|
|
|
|
+ ctx.provide('connection', { api: { skills: { list: listOk(CATALOG) } } })
|
|
|
|
|
+ ctx.provide('sessions', { subagentAddress: () => undefined })
|
|
|
|
|
+ const presentation = providePresentation(ctx)
|
|
|
|
|
+ await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
|
|
+ const entry = presentation.slots.entries('conversation.chat.toolview')[0]
|
|
|
|
|
+ expect(entry?.options).toMatchObject({ key: 'skill' })
|
|
|
|
|
+ expect(entry?.locale).toBe('skill')
|
|
|
|
|
+ expect(entry?.component).toBe(SkillToolRow)
|
|
|
|
|
+ expect(presentation.dictionaries).toEqual([{
|
|
|
|
|
+ namespace: 'skill', dictionaries: {
|
|
|
|
|
+ zh: {
|
|
|
|
|
+ 'row.running': '正在加载 skill',
|
|
|
|
|
+ 'row.failed': 'skill 加载失败',
|
|
|
|
|
+ 'row.stopped': 'skill 加载已中止',
|
|
|
|
|
+ 'row.instructions': '说明',
|
|
|
|
|
+ },
|
|
|
|
|
+ en: {
|
|
|
|
|
+ 'row.running': 'Loading skill',
|
|
|
|
|
+ 'row.failed': 'Skill load failed',
|
|
|
|
|
+ 'row.stopped': 'Skill load stopped',
|
|
|
|
|
+ 'row.instructions': 'Instructions',
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ }])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('registers the "/" skill source; disposal frees the name (HMR safety)', async () => {
|
|
it('registers the "/" skill source; disposal frees the name (HMR safety)', async () => {
|
|
@@ -74,6 +134,7 @@ describe('apply', () => {
|
|
|
ctx.provide('sessions', {})
|
|
ctx.provide('sessions', {})
|
|
|
await ctx.plugin(SlashService).await()
|
|
await ctx.plugin(SlashService).await()
|
|
|
ctx.provide('connection', { api: { skills: { list: listOk(CATALOG) } } })
|
|
ctx.provide('connection', { api: { skills: { list: listOk(CATALOG) } } })
|
|
|
|
|
+ const presentation = providePresentation(ctx)
|
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
|
await fiber.await()
|
|
await fiber.await()
|
|
|
const slash = ctx.get('slash') as SlashService
|
|
const slash = ctx.get('slash') as SlashService
|
|
@@ -88,6 +149,8 @@ describe('apply', () => {
|
|
|
// …and fiber teardown releases it.
|
|
// …and fiber teardown releases it.
|
|
|
await fiber.dispose()
|
|
await fiber.dispose()
|
|
|
expect(() => slash.registerSource(rival)).not.toThrow()
|
|
expect(() => slash.registerSource(rival)).not.toThrow()
|
|
|
|
|
+ expect(presentation.slots.entries('conversation.chat.toolview')).toHaveLength(0)
|
|
|
|
|
+ expect(presentation.localeDisposed).toBe(true)
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|