Phase 7: skip missing skills with ui.notify warning on expand.

buildReinjectBlocks checks registration and file presence before expandSkill; warns via ctx.ui when hasUI, no-op in RPC/print mode.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-17 11:59:12 +07:00
parent dc73ea9747
commit 9a197aee10
+41 -16
View File
@@ -5,6 +5,7 @@ import type {
SessionCompactEvent,
Skill,
} from "@earendil-works/pi-coding-agent";
import { existsSync } from "node:fs";
import { expandSkill } from "./expand.js";
import {
filterSkillsNeedingReinject,
@@ -16,6 +17,13 @@ import type { ExtensionState } from "./state.js";
export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject";
function notifySkippedSkill(ctx: ExtensionContext | undefined, skillName: string, reason: string): void {
if (!ctx?.hasUI) {
return;
}
ctx.ui.notify(`skill-reinject: skipped "${skillName}" — ${reason}`, "warning");
}
/** Names still registered in resourceLoader (SPEC §5.2). */
export function registeredSkillNames(skills: readonly Pick<Skill, "name">[]): ReadonlySet<string> {
return new Set(skills.map((skill) => skill.name));
@@ -66,25 +74,38 @@ export function buildReinjectBlocks(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
): string[] {
const registeredByName = new Map(registeredSkills.map((skill) => [skill.name, skill]));
const blocks: string[] = [];
for (const name of skillNames) {
const tracked = state.skills.find((skill) => skill.name === name);
const registered = registeredByName.get(name);
if (!tracked || !registered) {
if (!tracked) {
continue;
}
blocks.push(
expandSkill(
{
name: tracked.name,
filePath: registered.filePath,
baseDir: registered.baseDir,
},
settings.suffix,
),
);
const registered = registeredByName.get(name);
if (!registered) {
notifySkippedSkill(ctx, name, "no longer registered");
continue;
}
if (!existsSync(registered.filePath)) {
notifySkippedSkill(ctx, name, "SKILL.md not found on disk");
continue;
}
try {
blocks.push(
expandSkill(
{
name: tracked.name,
filePath: registered.filePath,
baseDir: registered.baseDir,
},
settings.suffix,
),
);
} catch {
notifySkippedSkill(ctx, name, "SKILL.md not readable");
}
}
return blocks;
}
@@ -95,8 +116,9 @@ export function buildDeferredReinjectContent(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
): string {
return buildReinjectBlocks(pendingNames, state, settings, registeredSkills).join("\n\n");
return buildReinjectBlocks(pendingNames, state, settings, registeredSkills, ctx).join("\n\n");
}
/** Immediate path when agent is idle: first block triggers turn, rest queue as followUp (SPEC §6.5.2). */
@@ -106,8 +128,9 @@ export function sendImmediateReinjectIdle(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
): void {
const blocks = buildReinjectBlocks(skillNames, state, settings, registeredSkills);
const blocks = buildReinjectBlocks(skillNames, state, settings, registeredSkills, ctx);
blocks.forEach((block, index) => {
pi.sendUserMessage(block, index === 0 ? undefined : { deliverAs: "followUp" });
});
@@ -120,8 +143,9 @@ export function sendImmediateReinjectAllFollowUp(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
): void {
for (const block of buildReinjectBlocks(skillNames, state, settings, registeredSkills)) {
for (const block of buildReinjectBlocks(skillNames, state, settings, registeredSkills, ctx)) {
pi.sendUserMessage(block, { deliverAs: "followUp" });
}
}
@@ -134,12 +158,13 @@ export function tryConsumeDeferredReinject(
state: ExtensionState,
settings: SkillReinjectSettings,
registeredSkills: readonly Pick<Skill, "name" | "filePath" | "baseDir">[],
ctx?: ExtensionContext,
): BeforeAgentStartEventResult | undefined {
if (state.pendingReinject.length === 0) {
return undefined;
}
const pendingNames = [...state.pendingReinject];
const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills);
const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills, ctx);
if (!content) {
state.pendingReinject = [];
return undefined;