feat(client): inject slot declaration lifetimes

This commit is contained in:
imccyu
2026-08-05 23:38:29 +08:00
parent bb53e25ed0
commit 86965b053c
100 changed files with 1065 additions and 837 deletions

View File

@@ -39,12 +39,8 @@ export interface PlanChipInjected {
exitPlanMode: () => Promise<string | null>
}
/**
* Required services: the seat's slot registry, the transport, the copy's
* locale registry, and the conversation service whose presence guarantees
* the seat is declared.
*/
export const inject = ['slots', 'connection', 'conversation', 'locale']
/** Required services: the seat's slot registry, transport, and locale registry. */
export const inject = ['slots', 'connection', 'locale']
/**
* Client plugin body: register the plan chip over the command channel.
@@ -53,7 +49,7 @@ export const inject = ['slots', 'connection', 'conversation', 'locale']
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-plan: dictionaries')
ctx.effect(() => ctx.slots.register({
ctx.slots.inject('conversation.input.plan', () => ctx.slots.register({
name: 'conversation.input.plan',
locale: NS,
inject: (sessionId: SessionId): PlanChipInjected => ({
@@ -66,5 +62,5 @@ export function apply(ctx: ClientContext): void {
return null
},
}),
}, PlanChip), 'ui-plan: composer plan chip registration')
}, PlanChip))
}

View File

@@ -28,28 +28,32 @@ async function bench() {
const execute = vi.fn((_payload: { sessionId: SessionId; line: string }) =>
Promise.resolve({ result: { ok: true as const, value: { matched: true as const, commandId: 'c1' } } }))
ctx.provide('connection', { api: { commands: { execute } } })
ctx.provide('conversation', {})
ctx.provide('locale', new LocaleService(ctx))
return { ctx, slots, execute }
}
describe('ui-plan browser apply', () => {
it('declares every service it binds', () => {
expect(inject).toEqual(['slots', 'connection', 'conversation', 'locale'])
expect(inject).toEqual(['slots', 'connection', 'locale'])
})
it('node-half apply is an intentional no-op', () => {
expect(() => { nodeApply() }).not.toThrow()
})
it('fails loud when conversation did not declare the plan seat', async () => {
it('waits until conversation declares the plan seat', async () => {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
ctx.provide('connection', {})
ctx.provide('conversation', {})
ctx.provide('locale', new LocaleService(ctx))
await expect(ctx.plugin({ inject: [...inject], apply }))
.rejects.toThrow(/slot "conversation.input.plan" is not declared/)
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(ctx.slots.entries('conversation.input.plan')).toHaveLength(0)
ctx.slots.register({
name: 'root', children: { 'conversation.input.plan': { kind: 'single', scope: 'session' } },
} as never, () => null)
await Promise.resolve()
expect(ctx.slots.entries('conversation.input.plan')).toHaveLength(1)
})
it('registers the chip, executes /plan off, and unregisters on teardown', async () => {