feat(coding-agent): make skill invocation messages collapsible

- Add ParsedSkillBlock interface and parseSkillBlock() function
- Change skill expansion to use XML-style <skill> tags
- Add SkillInvocationMessageComponent for collapsible display
- Collapsed: single line with skill name and expand hint
- User message rendered separately after skill block

Fixes #894
This commit is contained in:
Mario Zechner 2026-01-22 22:29:24 +01:00
parent f54e71999f
commit 7868b25a2b
6 changed files with 113 additions and 6 deletions

View file

@ -70,6 +70,33 @@ import { buildSystemPrompt } from "./system-prompt.js";
import type { BashOperations } from "./tools/bash.js";
import { createAllTools } from "./tools/index.js";
// ============================================================================
// Skill Block Parsing
// ============================================================================
/** Parsed skill block from a user message */
export interface ParsedSkillBlock {
name: string;
location: string;
content: string;
userMessage: string | undefined;
}
/**
* Parse a skill block from message text.
* Returns null if the text doesn't contain a skill block.
*/
export function parseSkillBlock(text: string): ParsedSkillBlock | null {
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
if (!match) return null;
return {
name: match[1],
location: match[2],
content: match[3],
userMessage: match[4]?.trim() || undefined,
};
}
/** Session-specific events that extend the core AgentEvent */
export type AgentSessionEvent =
| AgentEvent
@ -796,9 +823,8 @@ export class AgentSession {
try {
const content = readFileSync(skill.filePath, "utf-8");
const body = stripFrontmatter(content).trim();
const header = `Skill location: ${skill.filePath}\nReferences are relative to ${skill.baseDir}.`;
const skillMessage = `${header}\n\n${body}`;
return args ? `${skillMessage}\n\n---\n\nUser: ${args}` : skillMessage;
const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;
return args ? `${skillBlock}\n\n${args}` : skillBlock;
} catch (err) {
// Emit error like extension commands do
this._extensionRunner?.emitError({