fix(coding-agent): identify user-invoked skills and expose skill directory
User-invoked skills (typed /skill:, steered, follow-up, interrupted/ resumed via compaction, ACP, RPC) only appended a bare "Skill: <path>" line, so the model neither learned the user had invoked that specific skill nor where the skill directory was. Relative paths in skill bodies (scripts/, templates/) could not be resolved. Route all user-invoked paths through a self-identifying, baseDir-aware prompt template; keep hidden autoload skills on the minimal non-user format. Interactive skillCommands now carries the loaded Skill object instead of a bare path so baseDir flows through without reconstruction. The invocation kind defaults to "user" to keep buildSkillPromptMessage source-compatible. Op: correct Restores: spec:user-invoked-skill-prompt-self-identifies-and-exposes-skill-directory
This commit is contained in:
@@ -845,7 +845,7 @@ export class AcpAgent implements Agent {
|
||||
if (!skill) {
|
||||
return false;
|
||||
}
|
||||
const built = await buildSkillPromptMessage(skill, args);
|
||||
const built = await buildSkillPromptMessage(skill, args, "user");
|
||||
await record.session.promptCustomMessage({
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: built.message,
|
||||
|
||||
@@ -1160,21 +1160,6 @@ export class CommandController {
|
||||
this.ctx.showStatus(formatShakeSummary(result));
|
||||
}
|
||||
|
||||
async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
||||
try {
|
||||
const content = await Bun.file(skillPath).text();
|
||||
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
||||
const metaLines = [`Skill: ${skillPath}`];
|
||||
if (args) {
|
||||
metaLines.push(`User: ${args}`);
|
||||
}
|
||||
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
||||
await this.ctx.session.prompt(message);
|
||||
} catch (err) {
|
||||
this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async executeCompaction(
|
||||
customInstructionsOrOptions?: string | CompactOptions,
|
||||
isAuto = false,
|
||||
|
||||
@@ -65,6 +65,7 @@ import type {
|
||||
ExtensionWidgetOptions,
|
||||
} from "../extensibility/extensions";
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import type { Skill } from "../extensibility/skills";
|
||||
import { loadSlashCommands } from "../extensibility/slash-commands";
|
||||
import { type GuidedGoalMessage, runGuidedGoalTurn } from "../goals/guided-setup";
|
||||
import type { Goal, GoalModeState } from "../goals/state";
|
||||
@@ -478,7 +479,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
lastStatusSpacer: Spacer | undefined = undefined;
|
||||
lastStatusText: Text | undefined = undefined;
|
||||
fileSlashCommands: Set<string> = new Set();
|
||||
skillCommands: Map<string, string> = new Map();
|
||||
skillCommands: Map<string, Skill> = new Map();
|
||||
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
||||
collabHost?: CollabHost;
|
||||
collabGuest?: CollabGuestLink;
|
||||
@@ -687,7 +688,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
if (settings.get("skills.enableSkillCommands")) {
|
||||
for (const skill of this.session.skills) {
|
||||
const commandName = `skill:${skill.name}`;
|
||||
this.skillCommands.set(commandName, skill.filePath);
|
||||
this.skillCommands.set(commandName, skill);
|
||||
skillCommandList.push({ name: commandName, description: skill.description });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function tryRunRpcSkillCommand(
|
||||
const skillName = commandName.slice("skill:".length);
|
||||
const skill = session.skills.find(candidate => candidate.name === skillName);
|
||||
if (!skill) return false;
|
||||
const built = await buildSkillPromptMessage(skill, args);
|
||||
const built = await buildSkillPromptMessage(skill, args, "user");
|
||||
await session.promptCustomMessage({
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: built.message,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { buildSkillPromptMessage } from "../extensibility/skills";
|
||||
import { type CustomMessage, SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../session/messages";
|
||||
import type { InteractiveModeContext } from "./types";
|
||||
|
||||
@@ -61,32 +62,19 @@ export async function buildSkillCommandPrompt(
|
||||
): Promise<BuiltSkillCommandPrompt | undefined> {
|
||||
const parsed = parseSkillCommand(text);
|
||||
if (!parsed) return undefined;
|
||||
const skillPath = ctx.skillCommands.get(parsed.commandName);
|
||||
if (!skillPath) return undefined;
|
||||
const skill = ctx.skillCommands.get(parsed.commandName);
|
||||
if (!skill) return undefined;
|
||||
|
||||
const content = await Bun.file(skillPath).text();
|
||||
const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
|
||||
const metaLines = [`Skill: ${skillPath}`];
|
||||
if (parsed.args) {
|
||||
metaLines.push(`User: ${parsed.args}`);
|
||||
}
|
||||
const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
|
||||
const textBlock: TextContent = { type: "text", text: message };
|
||||
const promptContent = images && images.length > 0 ? [textBlock, ...images] : message;
|
||||
const skillName = parsed.commandName.slice("skill:".length);
|
||||
const details: SkillPromptDetails = {
|
||||
name: skillName || parsed.commandName,
|
||||
path: skillPath,
|
||||
args: parsed.args || undefined,
|
||||
lineCount: body ? body.split("\n").length : 0,
|
||||
};
|
||||
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
||||
const textBlock: TextContent = { type: "text", text: built.message };
|
||||
const promptContent = images && images.length > 0 ? [textBlock, ...images] : built.message;
|
||||
|
||||
return {
|
||||
message: {
|
||||
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
||||
content: promptContent,
|
||||
display: true,
|
||||
details,
|
||||
details: built.details,
|
||||
attribution: "user",
|
||||
},
|
||||
options: { streamingBehavior, queueChipText: text },
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
ExtensionWidgetOptions,
|
||||
} from "../extensibility/extensions";
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import type { Skill } from "../extensibility/skills";
|
||||
import type { MCPManager } from "../mcp";
|
||||
import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
|
||||
import type { AgentSession } from "../session/agent-session";
|
||||
@@ -206,7 +207,7 @@ export interface InteractiveModeContext {
|
||||
lastStatusSpacer: Spacer | undefined;
|
||||
lastStatusText: Text | undefined;
|
||||
fileSlashCommands: Set<string>;
|
||||
skillCommands: Map<string, string>;
|
||||
skillCommands: Map<string, Skill>;
|
||||
oauthManualInput: OAuthManualInputManager;
|
||||
todoPhases: TodoPhase[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user