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:
Turtle
2026-08-06 14:48:35 +08:00
2425 changed files with 46385 additions and 24275 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
客户端命令业务面(`ctx.command`):以会话为 key 的命令目录缓存、带 matchSpacematchEnter 裁决钩子的 `/` 命令 source、三型派发executepopupSelectleadingInput以及面向业务包的 popupSelect 注册面。契约:[Web 命令业务面 Agent Noteagent 决策记录)](../../../.agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.zh.md)。
客户端命令业务面(`ctx.command`):以会话为 key 的命令目录缓存、带 matchSpacematchEnter 裁决钩子的 `/` 命令 source、三型派发executepopupSelectleadingInput以及面向业务包的 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-louddecoration装饰则把裸调用 popup 挂在**已存在的** host 命令上——host 保留目录行、带参 claimspace / 带参 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 输出行是仅剩的呈现面。

View File

@@ -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"
]
}

View File

@@ -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))
})
}

View File

@@ -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/)
})