src / render.ts
import type { Skill } from "./types";
const TASK_PLACEHOLDER = "(no task text — ask the user what to do with this skill)";
/** Escapes a value for use inside a double-quoted tag attribute. */
function attr(v: string): string {
return v.replace(/&/g, "&").replace(/"/g, """);
}
export function renderSkillBlock(
skill: Skill,
body: string,
opts: { source: "explicit" | "jev"; p?: number; maxInjectedChars: number },
): string {
let content = body.trim();
if (content.length > opts.maxInjectedChars) {
content = `${content.slice(0, opts.maxInjectedChars)}\n[truncated — read the rest with read_skill_file("${skill.name}")]`;
}
const pAttr = opts.source === "jev" && opts.p !== undefined ? ` p="${opts.p.toFixed(2)}"` : "";
return `<skill name="${attr(skill.name)}" path="${attr(skill.dir)}" source="${opts.source}"${pAttr}>\n${content}\n</skill>`;
}
export function renderActiveReference(skill: Skill): string {
return `<skill_active name="${attr(skill.name)}" path="${attr(skill.dir)}">Instructions were given earlier in this conversation and still apply. Use read_skill_file to reload them if needed.</skill_active>`;
}
export function renderNote(): string {
return "<skill_note>Files under a skill (references/, scripts/) can be read with read_skill_file(skill, path). To run a script use shell_run if that tool is available; otherwise show the user the exact command to run.</skill_note>";
}
export function renderHint(skills: Skill[]): string {
const lines = skills.map(s => `- ${s.name}: ${s.description} (${s.dir})`);
return ["<available_skills>", ...lines, "Call read_skill_file(name) to load a skill's instructions.", "</available_skills>"].join("\n");
}
export function renderError(name: string, skills: Skill[]): string {
return `<skill_error>No skill named "${name}". Available: ${skills.map(s => s.name).join(", ")}</skill_error>`;
}
/** Explicit activation of a known skill whose SKILL.md could not be read. */
export function renderReadError(name: string, reason: string): string {
return `<skill_error>Could not read SKILL.md for "${name}": ${reason}</skill_error>`;
}
export function renderMessage(blocks: string[], userText: string): string {
const task = userText.trim() === "" ? TASK_PLACEHOLDER : userText;
return [...blocks, `<user_task>\n${task}\n</user_task>`].join("\n\n");
}
/** True if `text` already contains an injected `<skill name="…">` block for this exact name. */
export function hasSkillMarker(text: string, name: string): boolean {
return text.includes(`<skill name="${attr(name)}" `);
}
src / render.ts
import type { Skill } from "./types";
const TASK_PLACEHOLDER = "(no task text — ask the user what to do with this skill)";
/** Escapes a value for use inside a double-quoted tag attribute. */
function attr(v: string): string {
return v.replace(/&/g, "&").replace(/"/g, """);
}
export function renderSkillBlock(
skill: Skill,
body: string,
opts: { source: "explicit" | "jev"; p?: number; maxInjectedChars: number },
): string {
let content = body.trim();
if (content.length > opts.maxInjectedChars) {
content = `${content.slice(0, opts.maxInjectedChars)}\n[truncated — read the rest with read_skill_file("${skill.name}")]`;
}
const pAttr = opts.source === "jev" && opts.p !== undefined ? ` p="${opts.p.toFixed(2)}"` : "";
return `<skill name="${attr(skill.name)}" path="${attr(skill.dir)}" source="${opts.source}"${pAttr}>\n${content}\n</skill>`;
}
export function renderActiveReference(skill: Skill): string {
return `<skill_active name="${attr(skill.name)}" path="${attr(skill.dir)}">Instructions were given earlier in this conversation and still apply. Use read_skill_file to reload them if needed.</skill_active>`;
}
export function renderNote(): string {
return "<skill_note>Files under a skill (references/, scripts/) can be read with read_skill_file(skill, path). To run a script use shell_run if that tool is available; otherwise show the user the exact command to run.</skill_note>";
}
export function renderHint(skills: Skill[]): string {
const lines = skills.map(s => `- ${s.name}: ${s.description} (${s.dir})`);
return ["<available_skills>", ...lines, "Call read_skill_file(name) to load a skill's instructions.", "</available_skills>"].join("\n");
}
export function renderError(name: string, skills: Skill[]): string {
return `<skill_error>No skill named "${name}". Available: ${skills.map(s => s.name).join(", ")}</skill_error>`;
}
/** Explicit activation of a known skill whose SKILL.md could not be read. */
export function renderReadError(name: string, reason: string): string {
return `<skill_error>Could not read SKILL.md for "${name}": ${reason}</skill_error>`;
}
export function renderMessage(blocks: string[], userText: string): string {
const task = userText.trim() === "" ? TASK_PLACEHOLDER : userText;
return [...blocks, `<user_task>\n${task}\n</user_task>`].join("\n\n");
}
/** True if `text` already contains an injected `<skill name="…">` block for this exact name. */
export function hasSkillMarker(text: string, name: string): boolean {
return text.includes(`<skill name="${attr(name)}" `);
}