Files
deepseek-harness/packages/ui/tui/src/chat/skill-invocation.ts
Turtle 46bbbce109 refactor(tui): split createTuiChat into chat/ sub-controllers
Extract model-command, questions, and resume sub-machines from the
~1600-line createTuiChat closure into src/chat/ factories that take
explicit dependency bundles (shared ChatChannelDeps/ChannelNotice).
Reorganize src/ so chat/ holds all chat-channel concerns (former input
and session/ files move under it); xml-tool-output moves to components/;
TuiRuntime/TuiResumeHost move to runtime.ts. index.ts drops 2067->~1530
lines. Behavior identical: 167 tests and all TUI snapshots pass unchanged.
2026-07-27 21:04:11 +08:00

68 lines
3.0 KiB
TypeScript

/**
* Manual `/skill:<name> [instructions]` parsing and model-visible rendering for
* the terminal front door.
* @module @deepseek-ai/dsh-tui/chat/skill-invocation
*/
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { SkillDefinition, SkillResourceBase } from '@deepseek-ai/dsh-skill'
/** Prefix that marks an editor submission as a manual skill invocation. */
export const SKILL_COMMAND_PREFIX = '/skill:'
/** Parsed `/skill:<name> [instructions]` submission; `name` is empty when the prefix carries no name. */
export interface ParsedSkillCommand {
/** Skill name typed after `/skill:`, up to the first space. */
name: string
/** Trimmed text after the name; empty when none was typed. */
instructions: string
}
/**
* Split a `/skill:<name> [instructions]` submission into its name and trailing instructions.
* @param text - trimmed submission that starts with {@link SKILL_COMMAND_PREFIX}.
* @returns the skill name and any trailing instructions.
*/
export function parseSkillCommand(text: string): ParsedSkillCommand {
const rest = text.slice(SKILL_COMMAND_PREFIX.length)
const spaceIndex = rest.indexOf(' ')
if (spaceIndex === -1) return { name: rest, instructions: '' }
return { name: rest.slice(0, spaceIndex), instructions: rest.slice(spaceIndex + 1).trim() }
}
/** Model-visible line locating a manually invoked skill's relative resources, or `undefined` when the provider has no base. */
function skillResourceReference(base: SkillResourceBase | undefined): string | undefined {
if (base === undefined) return undefined
switch (base.kind) {
case 'directory':
return `References in this skill are relative to ${base.path}.`
case 'url':
return `References in this skill are relative to ${base.url}.`
case 'opaque':
return base.description
default:
return assertNever(base, 'SkillResourceBase.kind')
}
}
/**
* Render a manually invoked skill into the model-visible user-message text. The
* `<skill>` block carries the body and, when the provider supplies one, its
* resource base; the trimmed `instructions` follow the block as the user's
* request for this turn. The name is registry-validated kebab-case
* (the skill registry rejects any other) and the resource base is trusted
* same-process provider prose, so — unlike the model-facing `dsh-tool-skill`
* result, which escapes for a tool channel — this user turn is assembled raw.
* @param skill - the loaded skill definition.
* @param instructions - trimmed text typed after `/skill:<name>`; empty when absent.
* @returns the user-message text delivered to the agent.
*/
export function renderSkillInvocation(skill: SkillDefinition, instructions: string): string {
const lines = [`<skill name="${skill.name}">`]
const reference = skillResourceReference(skill.resourceBase)
if (reference !== undefined) lines.push(reference, '')
lines.push(skill.content, '</skill>')
const block = lines.join('\n')
return instructions === '' ? block : `${block}\n\n${instructions}`
}