Move skill command handling to AgentSession, update docs

- Skill commands (/skill:name) now expanded in AgentSession instead of
  interactive mode, enabling them in RPC and print modes
- Input event can now intercept /skill:name before expansion
- Updated extensions.md with clearer input event docs and processing order
- Updated rpc.md: hook -> extension terminology, added skill expansion mentions
- Added PR attribution to changelog entries for #761
This commit is contained in:
Mario Zechner 2026-01-16 03:01:08 +01:00
parent 3e5d91f287
commit b4a05cbcab
7 changed files with 211 additions and 420 deletions

View file

@ -13,6 +13,7 @@
* Modes use this class and add their own I/O layer on top.
*/
import { readFileSync } from "node:fs";
import type {
Agent,
AgentEvent,
@ -25,6 +26,7 @@ import type { AssistantMessage, ImageContent, Message, Model, TextContent } from
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
import { getAuthPath } from "../config.js";
import { theme } from "../modes/interactive/theme/theme.js";
import { stripFrontmatter } from "../utils/frontmatter.js";
import { type BashResult, executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor.js";
import {
type CompactionResult,
@ -569,7 +571,7 @@ export class AgentSession {
}
}
// Emit input event for extension interception (before template expansion)
// Emit input event for extension interception (before skill/template expansion)
let currentText = text;
let currentImages = options?.images;
if (this._extensionRunner?.hasHandlers("input")) {
@ -587,10 +589,12 @@ export class AgentSession {
}
}
// Expand file-based prompt templates if requested
const expandedText = expandPromptTemplates
? expandPromptTemplate(currentText, [...this._promptTemplates])
: currentText;
// Expand skill commands (/skill:name args) and prompt templates (/template args)
let expandedText = currentText;
if (expandPromptTemplates) {
expandedText = this._expandSkillCommand(expandedText);
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
}
// If streaming, queue via steer() or followUp() based on option
if (this.isStreaming) {
@ -718,10 +722,42 @@ export class AgentSession {
}
}
/**
* Expand skill commands (/skill:name args) to their full content.
* Returns the expanded text, or the original text if not a skill command or skill not found.
* Emits errors via extension runner if file read fails.
*/
private _expandSkillCommand(text: string): string {
if (!text.startsWith("/skill:")) return text;
const spaceIndex = text.indexOf(" ");
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
const skill = this._skills.find((s) => s.name === skillName);
if (!skill) return text; // Unknown skill, pass through
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;
} catch (err) {
// Emit error like extension commands do
this._extensionRunner?.emitError({
extensionPath: skill.filePath,
event: "skill_expansion",
error: err instanceof Error ? err.message : String(err),
});
return text; // Return original on error
}
}
/**
* Queue a steering message to interrupt the agent mid-run.
* Delivered after current tool execution, skips remaining tools.
* Expands file-based prompt templates. Errors on extension commands.
* Expands skill commands and prompt templates. Errors on extension commands.
* @throws Error if text is an extension command
*/
async steer(text: string): Promise<void> {
@ -730,8 +766,9 @@ export class AgentSession {
this._throwIfExtensionCommand(text);
}
// Expand file-based prompt templates
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
// Expand skill commands and prompt templates
let expandedText = this._expandSkillCommand(text);
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
await this._queueSteer(expandedText);
}
@ -739,7 +776,7 @@ export class AgentSession {
/**
* Queue a follow-up message to be processed after the agent finishes.
* Delivered only when agent has no more tool calls or steering messages.
* Expands file-based prompt templates. Errors on extension commands.
* Expands skill commands and prompt templates. Errors on extension commands.
* @throws Error if text is an extension command
*/
async followUp(text: string): Promise<void> {
@ -748,8 +785,9 @@ export class AgentSession {
this._throwIfExtensionCommand(text);
}
// Expand file-based prompt templates
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
// Expand skill commands and prompt templates
let expandedText = this._expandSkillCommand(text);
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
await this._queueFollowUp(expandedText);
}