Files
deepseek-harness/packages/client/ui-command/src/client/index.ts
T
imccyu 83c2115de8 refactor(client): command decorations replace the hostBacked contribution mode
A popup on a host command is not a second command — it is what that
command's BARE invocation does on this client. CommandContribution loses
hostBacked (contributions are pure client commands again; a host-name
collision fails loud, unchanged for /model), and the contract gains
CommandDecoration + command.decorate(): key = the HOST command name, no
catalog row, no claim participation. Dispatch consults decorations only on
the bare paths (menu pick / bare enter) after the host row resolves; space
and argued enter never see them — the two edges hostBacked had to guard
explicitly hold by construction in the decoration model. A decorated name
with no host row in the session's directory never fires (a decoration
cannot manufacture a command).

ui-permission switches register→decorate with zero behavior change
(options still read the permissions projection; a pick still submits
'/permission <preset>'). Specs rewrite to the decoration semantics: no
catalog row, bare-enter popup vs argued-enter host claim, space host
claim, no-host-row miss, unavailable fall-through, duplicate fail-loud.
2026-07-29 12:01:36 +08:00

62 lines
2.7 KiB
TypeScript

/**
* Command UI plugin, browser half: CommandService (`ctx.command`) owning the
* capability-keyed directory cache, the '/' command source, the client
* contribution registry, and the per-session popupSelect controllers; the
* popupSelect shell self-registers into conversation.input.overlay with
* per-session resolution.
*/
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
// Type-only: pulls the 'conversation.input.overlay' SlotMap declaration (the
// key's owner) into this program so the overlay registration below typechecks
// against the real declaration — no runtime edge to ui-conversation.
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
import { CommandService } from './service.ts'
import type { PopupSelectInjected } from './PopupSelectView.tsx'
import { PopupSelectView } from './PopupSelectView.tsx'
export { CommandService } from './service.ts'
export { CommandDirectory } from './directory.ts'
export type { CommandDescriptor, DirectoryStatus } from './directory.ts'
export { filterOptions, PopupSelectController } from './popup.ts'
export type { PopupSelectDeps, PopupSpec, PopupState, TokenSegment } from './popup.ts'
export type { PopupSelectInjected } from './PopupSelectView.tsx'
export type {
CommandContribution, CommandDecoration, CommandServiceContract, CommandUiSpec, SelectOption,
} from './contract.ts'
declare module 'cordis' {
interface Context {
command: CommandService
}
}
/** Required services: the '/' source registry plus the scope + wire faces the service reads. */
export const inject = ['slash', 'sessions', 'connection']
/**
* Client plugin body: mount the service, then register the popupSelect shell
* into the input overlay once its declarer is up.
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
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) => {
const command = scope.command
const sessions = scope.sessions
scope.effect(() => scope.slots.register({
name: 'conversation.input.overlay',
id: 'command-popup',
order: 1,
inject: (sessionId): PopupSelectInjected => {
const actx = sessions.scope(sessionId)
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')
})
}