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.
62 lines
2.7 KiB
TypeScript
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')
|
|
})
|
|
}
|