Merge remote-tracking branch 'origin/master' into codex/web-slash-fuzzy-search
# Conflicts: # packages/client/ui-command/README.i18n.yaml
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-command/README.md
|
||||
README.md: 37df56c815c2dfd1d54a9dc0be4e28363cc8b66c
|
||||
README.zh.md: c90463dcac0095231327fa0fb7de1875457b73bc
|
||||
README.md: bc7386c8fca3b5c623473328bee6322fa7295277
|
||||
README.zh.md: 478ee4ccee4558c70075baa45ab34ac6e3d71617
|
||||
|
||||
@@ -24,5 +24,4 @@ None directly; this package neither assembles nor sends a provider request. Comm
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **The popupSelect shell has no shipped business consumer** — model selection (host `selectModel`) is the design's reference case and lands with its own feature work; until then the shell is exercised by package tests only.
|
||||
- **Detached-result notices fall back to the console off-session** — the fire-and-forget paths route results to the triggering session's composer via `SessionInput.notify`; after session teardown the console line is the only remaining surface.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
客户端命令业务面(`ctx.command`):以会话为 key 的命令目录缓存、带 matchSpace/matchEnter 裁决钩子的 `/` 命令 source、三型派发(execute/popupSelect/leadingInput),以及面向业务包的 popupSelect 注册面。契约:[Web 命令业务面 Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md)。
|
||||
客户端命令业务面(`ctx.command`):以会话为 key 的命令目录缓存、带 matchSpace/matchEnter 裁决钩子的 `/` 命令 source、三型派发(execute/popupSelect/leadingInput),以及面向业务包的 popupSelect 注册面。契约:[Web 命令业务面 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md)。
|
||||
|
||||
`src/client/contract.ts` 是冻结的业务表层:`CommandServiceContract.register(name, spec)` 与 `decorate(name, spec)` 是业务包消费的全部内容;`CommandUiSpec{options, onSelect}` 让 popup 数据自给自足——壳组件归本包所有,业务永远见不到它。contribution 是 client 自有命令(与 host 同名碰撞即 fail-loud);decoration(装饰)则把裸调用 popup 挂在**已存在的** host 命令上——host 保留目录行、带参 claim(space / 带参 enter)与生命周期记账,被装饰的名字若在会话目录中无 host 行则装饰永不触发。命令三型按每次派发派生,绝不在注册时定型:带 `input` 的 host descriptor 是 leadingInput,注册了 `CommandUiSpec` 的是 popupSelect,其余全部是 execute。
|
||||
|
||||
@@ -24,5 +24,4 @@
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **popupSelect 壳还没有已上架的业务消费者**:模型选择(host `selectModel`)是设计的参照用例,将随其自身的功能工作落地;在此之前,壳只由包测试演练。
|
||||
- **脱离会话后,detached result 的 notice 回退到 console**:fire-and-forget 路径经 `SessionInput.notify` 把结果送到触发会话的编辑器;会话拆除后,console 输出行是仅剩的呈现面。
|
||||
|
||||
@@ -69,8 +69,6 @@
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/client.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
"lib/types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -55,14 +55,10 @@ export const inject = ['slash', 'sessions', 'connection', 'locale']
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-command: dictionaries')
|
||||
ctx.plugin(CommandService)
|
||||
// Conditional mount, same seam as ui-slash's MenuView registration:
|
||||
// 'conversation.input.overlay' is declared by the conversation composer
|
||||
// entry, and the conversation service's presence is the registration-safe
|
||||
// signal that the declaration is on the ledger.
|
||||
ctx.inject(['slots', 'conversation', 'command', 'sessions'], (scope: ClientContext) => {
|
||||
ctx.inject(['slots', 'command', 'sessions'], (scope: ClientContext) => {
|
||||
const command = scope.command
|
||||
const sessions = scope.sessions
|
||||
scope.effect(() => scope.slots.register({
|
||||
scope.slots.inject('conversation.input.overlay', () => scope.slots.register({
|
||||
name: 'conversation.input.overlay',
|
||||
id: 'command-popup',
|
||||
order: 1,
|
||||
@@ -72,6 +68,6 @@ export function apply(ctx: ClientContext): void {
|
||||
if (actx === undefined) throw new Error(`ui-command: session "${String(sessionId)}" resolved no scope`)
|
||||
return { popup: command.popupFor(actx) }
|
||||
},
|
||||
}, PopupSelectView), 'ui-command: popupSelect overlay registration')
|
||||
}, PopupSelectView))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
* ui-command browser half on a real cordis Context with fake slash/slots
|
||||
* faces and real session scopes: the plugin body mounts CommandService as
|
||||
* `command`, the popupSelect shell registers into conversation.input.overlay
|
||||
* once the conversation seam is up with a per-session inject (sessionId →
|
||||
* through slot declaration injection with a per-session inject (sessionId →
|
||||
* scope → popupFor; unknown id fails loud), both fold up on fiber disposal
|
||||
* (HMR safety), and the service satisfies the frozen CommandServiceContract.
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createScope, scopeOf } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createScope, scopeOf, SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import type { CommandServiceContract } from '../src/client/contract.ts'
|
||||
@@ -21,7 +21,6 @@ const sid = (k: string): SessionId => k as SessionId
|
||||
async function bench() {
|
||||
const ctx = new Context()
|
||||
const sources = new Map<string, SlashSource>()
|
||||
const overlays = new Map<string, { inject: unknown }>()
|
||||
ctx.provide('slash', {
|
||||
registerSource(src: SlashSource) {
|
||||
sources.set(`${src.trigger} ${src.name}`, src)
|
||||
@@ -34,14 +33,10 @@ async function bench() {
|
||||
scopeOf: (c: Context) => scopeOf(c),
|
||||
})
|
||||
ctx.provide('connection', { api: { commands: { list: () => Promise.resolve({ result: { ok: true, value: { commands: [] } } }) } } })
|
||||
ctx.provide('slots', {
|
||||
register(options: { name: string; id?: string; inject?: unknown }) {
|
||||
const key = `${options.name}#${options.id ?? ''}`
|
||||
overlays.set(key, { inject: options.inject })
|
||||
return () => { overlays.delete(key) }
|
||||
},
|
||||
})
|
||||
ctx.provide('conversation', {})
|
||||
await ctx.plugin(SlotsService).await()
|
||||
ctx.slots.register({
|
||||
name: 'root', children: { 'conversation.input.overlay': { kind: 'list', scope: 'session' } },
|
||||
} as never, (() => null) as never)
|
||||
ctx.provide('locale', new LocaleService(ctx))
|
||||
const fiber = ctx.plugin({ inject: [...inject], apply })
|
||||
await fiber.await()
|
||||
@@ -50,7 +45,7 @@ async function bench() {
|
||||
scopes.set(sid(key), handle.ctx)
|
||||
return handle
|
||||
}
|
||||
return { ctx, fiber, sources, overlays, mint }
|
||||
return { ctx, fiber, sources, slots: ctx.slots, mint }
|
||||
}
|
||||
|
||||
describe('apply', () => {
|
||||
@@ -59,7 +54,7 @@ describe('apply', () => {
|
||||
})
|
||||
|
||||
it('mounts ctx.command, registers the source and the overlay entry, and folds up on disposal', async () => {
|
||||
const { ctx, fiber, sources, overlays } = await bench()
|
||||
const { ctx, fiber, sources, slots } = await bench()
|
||||
const command = ctx.get('command')
|
||||
expect(command).toBeInstanceOf(CommandService)
|
||||
// Frozen-contract conformance (compile-time check rides the assignment).
|
||||
@@ -67,18 +62,18 @@ describe('apply', () => {
|
||||
expect(typeof contract.register).toBe('function')
|
||||
expect(typeof contract.popupFor).toBe('function')
|
||||
expect([...sources.keys()]).toEqual(['/ command'])
|
||||
expect([...overlays.keys()]).toEqual(['conversation.input.overlay#command-popup'])
|
||||
expect(slots.entries('conversation.input.overlay').map(entry => entry.options.id)).toEqual(['command-popup'])
|
||||
await fiber.dispose()
|
||||
expect(sources.size).toBe(0)
|
||||
expect(overlays.size).toBe(0)
|
||||
expect(slots.entries('conversation.input.overlay')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('the overlay inject resolves the per-session popup controller by sessionId and fails loud on an unknown id', async () => {
|
||||
const { ctx, overlays, mint } = await bench()
|
||||
const { ctx, slots, mint } = await bench()
|
||||
const command = ctx.get('command') as CommandService
|
||||
const scope = mint('s1')
|
||||
const entry = overlays.get('conversation.input.overlay#command-popup')!
|
||||
const injectEntry = entry.inject as (sessionId: SessionId) => PopupSelectInjected
|
||||
const entry = slots.entries('conversation.input.overlay')[0]!
|
||||
const injectEntry = entry.inject as unknown as (sessionId: SessionId) => PopupSelectInjected
|
||||
expect(injectEntry(sid('s1')).popup).toBe(command.popupFor(scope.ctx))
|
||||
expect(() => injectEntry(sid('ghost'))).toThrow(/resolved no scope/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user