126 lines
5.2 KiB
TypeScript
126 lines
5.2 KiB
TypeScript
/**
|
|
* Subagent reference plugin, browser half: registers the '@' source —
|
|
* candidates filtered from the session list snapshot's running children
|
|
* (zero RPC; the list rides the plugin's root-context sessions service, the
|
|
* scoped session comes from the per-call projection), pick inserts the
|
|
* literal `@label ` text (decision 21: the draft carries plain text, chip
|
|
* visuals are derived by scanning against the source lexicon, and the
|
|
* prompt ships the same literal). Consumption semantics stay with future
|
|
* business work (design ledger). No adjudication hooks: subagent
|
|
* references never enter command adjudication.
|
|
*/
|
|
import type {
|
|
ClientContext, SessionId, SubagentAddress,
|
|
} from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { ComposerChainProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
import type { ClientSessionContext, SlashServiceContract, SlashSource } from '@deepseek-ai/dsh-client-ui-slash/client'
|
|
import { SubagentCatalogAction, type SubagentCatalogInjected } from './SubagentCatalogAction.tsx'
|
|
import {
|
|
SubagentReadOnlyComposer, type SubagentReadOnlyMatch,
|
|
} from './SubagentReadOnlyComposer.tsx'
|
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
import { en, NS, zh, type SubagentKey } from './locales.ts'
|
|
|
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
interface LocaleNamespaceMap {
|
|
/** Subagent catalog and read-only composer copy. */
|
|
'subagent': SubagentKey
|
|
}
|
|
}
|
|
|
|
export type {
|
|
SubagentCatalogActionProps, SubagentCatalogInjected,
|
|
} from './SubagentCatalogAction.tsx'
|
|
export type {
|
|
SubagentReadOnlyComposerProps, SubagentReadOnlyMatch,
|
|
} from './SubagentReadOnlyComposer.tsx'
|
|
|
|
/** Required services for references, conversation slots, and session navigation. */
|
|
export const inject = ['slash', 'sessions', 'slots', 'locale']
|
|
|
|
/** Claim the composer for one-shot history or an unavailable continuation owner. */
|
|
function selectReadOnlySubagent(owner: ComposerChainProps): SubagentReadOnlyMatch | null {
|
|
const subagent = owner.session?.subagent
|
|
if (subagent === undefined || subagent === null) return null
|
|
if (subagent.address.mode === 'one-shot') return { reason: 'one-shot' }
|
|
return subagent.parentAvailable ? null : { reason: 'parent-unavailable' }
|
|
}
|
|
|
|
/**
|
|
* Client plugin body: register the '@' subagent source over the root session list.
|
|
* @param ctx - client root context.
|
|
*/
|
|
export function apply(ctx: ClientContext): void {
|
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-subagent: dictionaries')
|
|
const sessions = ctx.sessions
|
|
// Child labels live on the session list (parentId lineage + displayTitle),
|
|
// not the conversation snapshot — the list store is the zero-RPC candidate feed.
|
|
const childLabels = (session: ClientSessionContext, query: string): string[] => {
|
|
const { byId } = sessions.list.getSnapshot()
|
|
return Object.values(byId)
|
|
.filter(child => child.parentId === session.sessionId && child.running && child.displayTitle.includes(query))
|
|
.map(child => child.displayTitle)
|
|
}
|
|
const source: SlashSource = {
|
|
trigger: '@',
|
|
name: 'subagent',
|
|
candidates(session, { query }) {
|
|
return Promise.resolve(childLabels(session, query).map(name => ({ name })))
|
|
},
|
|
lexicon(session) {
|
|
// The list snapshot is always warm — the full running-children roster.
|
|
return childLabels(session, '')
|
|
},
|
|
subscribeLexicon(_session, listener) {
|
|
// The roll derives from the list snapshot, so its change feed IS the list's.
|
|
return sessions.list.subscribe(listener)
|
|
},
|
|
onPick({ candidate }) {
|
|
// Decision 21: plain-text reference — the literal lands in the draft
|
|
// and ships to the model verbatim (trailing space closes the token).
|
|
// Legacy path (decision 21), retained for the removal cut, no longer reached:
|
|
// return { insert: { source: 'subagent', ref: candidate.name, label: candidate.name, clipboardText: `@${candidate.name}` } }
|
|
return { text: `@${candidate.name} ` }
|
|
},
|
|
codec: {
|
|
clipboardText: ref => `@${ref}`,
|
|
// TODO: serialize returns the raw label until the '@' consumption
|
|
// feature defines a model representation (design ledger).
|
|
serialize: ref => Promise.resolve(`@${ref}`),
|
|
},
|
|
}
|
|
const slash = ctx.get('slash') as SlashServiceContract
|
|
ctx.effect(() => slash.registerSource(source), 'ui-subagent: @ source')
|
|
|
|
const catalogActions = (_parentSessionId: SessionId): SubagentCatalogInjected => ({
|
|
openChild(address: SubagentAddress) {
|
|
sessions.openSubagent(address)
|
|
},
|
|
refresh(parentSessionId: SessionId) {
|
|
void sessions.refreshSubagents(parentSessionId)
|
|
},
|
|
setCatalogOpen(parentSessionId: SessionId, open: boolean) {
|
|
sessions.setSubagentCatalogOpen(parentSessionId, open)
|
|
},
|
|
})
|
|
ctx.slots.inject(
|
|
'conversation.session.header.actions',
|
|
() => ctx.slots.register({
|
|
name: 'conversation.session.header.actions',
|
|
id: 'subagent-catalog',
|
|
order: 10,
|
|
locale: NS,
|
|
inject: catalogActions,
|
|
}, SubagentCatalogAction),
|
|
)
|
|
ctx.slots.inject(
|
|
'conversation.composer',
|
|
() => ctx.slots.register({
|
|
name: 'conversation.composer',
|
|
priority: -10,
|
|
locale: NS,
|
|
select: selectReadOnlySubagent,
|
|
}, SubagentReadOnlyComposer),
|
|
)
|
|
}
|