import type { BeforeAgentStartEventResult, ExtensionAPI, ExtensionContext, SessionCompactEvent, Skill, } from "@earendil-works/pi-coding-agent"; import { expandSkill } from "./expand.js"; import { filterSkillsNeedingReinject, getKeptEntries, skillsPresentInKeptWindow, } from "./kept.js"; import type { SkillReinjectSettings } from "./settings.js"; import type { ExtensionState } from "./state.js"; export const DEFERRED_REINJECT_CUSTOM_TYPE = "skill-reinject:inject"; /** Names still registered in resourceLoader (SPEC §5.2). */ export function registeredSkillNames(skills: readonly Pick[]): ReadonlySet { return new Set(skills.map((skill) => skill.name)); } /** * Skill names to re-inject after compaction: tracked, absent from kept window, still registered (SPEC §5.2). * `registeredSkills` comes from resourceLoader — ExtensionContext has no getSkills(); wired in index.ts. */ export function planReinject( state: ExtensionState, _settings: SkillReinjectSettings, ctx: ExtensionContext, compactionEvent: SessionCompactEvent, registeredSkills: readonly Pick[], ): string[] { const branch = ctx.sessionManager.getBranch(); const keptEntries = getKeptEntries(branch, compactionEvent.compactionEntry.firstKeptEntryId); const trackedNames = state.skills.map((skill) => skill.name); const keptPresent = skillsPresentInKeptWindow(keptEntries, trackedNames); return filterSkillsNeedingReinject( state.skills, keptPresent, registeredSkillNames(registeredSkills), ); } /** Defer path on session_compact: queue planned skills without sendUserMessage (SPEC §6.5.1, §16.2). */ export function enqueueDeferredReinjectFromCompact( state: ExtensionState, settings: SkillReinjectSettings, ctx: ExtensionContext, compactionEvent: SessionCompactEvent, registeredSkills: readonly Pick[], ): void { state.pendingReinject = planReinject( state, settings, ctx, compactionEvent, registeredSkills, ); } /** Expanded skill-block messages in queue order (SPEC §5.3). */ export function buildReinjectBlocks( skillNames: readonly string[], state: ExtensionState, settings: SkillReinjectSettings, registeredSkills: readonly Pick[], ): 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) { continue; } blocks.push( expandSkill( { name: tracked.name, filePath: registered.filePath, baseDir: registered.baseDir, }, settings.suffix, ), ); } return blocks; } /** Combined skill-block user text for pending names in queue order (SPEC §5.3). */ export function buildDeferredReinjectContent( pendingNames: readonly string[], state: ExtensionState, settings: SkillReinjectSettings, registeredSkills: readonly Pick[], ): string { return buildReinjectBlocks(pendingNames, state, settings, registeredSkills).join("\n\n"); } /** Immediate path when agent is idle: first block triggers turn, rest queue as followUp (SPEC §6.5.2). */ export function sendImmediateReinjectIdle( pi: ExtensionAPI, skillNames: readonly string[], state: ExtensionState, settings: SkillReinjectSettings, registeredSkills: readonly Pick[], ): void { const blocks = buildReinjectBlocks(skillNames, state, settings, registeredSkills); blocks.forEach((block, index) => { pi.sendUserMessage(block, index === 0 ? undefined : { deliverAs: "followUp" }); }); } /** * Defer path on before_agent_start: inject one combined message, then clear queue (SPEC §6.5.1). * Returns undefined when pendingReinject is empty. */ export function tryConsumeDeferredReinject( state: ExtensionState, settings: SkillReinjectSettings, registeredSkills: readonly Pick[], ): BeforeAgentStartEventResult | undefined { if (state.pendingReinject.length === 0) { return undefined; } const pendingNames = [...state.pendingReinject]; const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills); if (!content) { state.pendingReinject = []; return undefined; } state.pendingReinject = []; return { message: { customType: DEFERRED_REINJECT_CUSTOM_TYPE, content, display: true, }, }; }