import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { access } from "node:fs/promises"; import { type TopOfMindConfig, DEFAULT_CONFIG, loadConfig, saveConfig, buildInjectionText, shouldInject, hasContent, resolvePath, } from "./utils.js"; // --------------------------------------------------------------------------- // Extension // --------------------------------------------------------------------------- export default function (pi: ExtensionAPI) { // ---- State --------------------------------------------------------------- let config: TopOfMindConfig = { ...DEFAULT_CONFIG }; // ---- Session start: load config ------------------------------------------ pi.on("session_start", async (_event, ctx) => { config = loadConfig(ctx.cwd); // If no text or file configured, disable by default if (!hasContent(config)) { config.enabled = false; } // Notify user of current state if (config.enabled) { ctx.ui.notify( `🧠 Top of Mind active (every ${config.frequency} turn(s))`, "info", ); } }); // ---- Inject top-of-mind after each turn ---------------------------------- // // turn_end fires after each LLM response + tool calls complete. // We call pi.sendMessage() to inject a custom message into the session. // With deliverAs: "steer" (the default), the message is queued and // delivered before the next LLM call — exactly when we want it. // pi.on("turn_end", async (_event, ctx) => { if (!config.enabled) return; config.turnCounter = (config.turnCounter ?? 0) + 1; // Check frequency if (!shouldInject(config.turnCounter, config.frequency)) return; try { const injectionText = await buildInjectionText(config, ctx.cwd); if (!injectionText) return; // Inject as a custom message. With default deliverAs: "steer", this // queues the message after the current tool calls finish, before the // next LLM call. The message participates in LLM context. pi.sendMessage({ customType: "top-of-mind", content: `[Top of Mind — persistent instructions]\n${injectionText}`, display: false, // Don't show in TUI, just inject into context }); } catch (err) { console.error("[pi-top-of-mind] Failed to inject context:", err); } }); // ---- Commands ------------------------------------------------------------ /** * /top-of-mind:prompt * Set the literal text to inject every turn. * With no arguments, shows current prompt text. */ pi.registerCommand("top-of-mind:prompt", { description: "Set persistent prompt text for every turn. Usage: /top-of-mind:prompt or /top-of-mind:prompt to show current.", handler: async (args, ctx) => { if (!args || args.trim() === "") { ctx.ui.notify( `Current prompt: ${config.text ?? "(not set)"}`, "info", ); return; } config.text = args.trim(); config.enabled = true; config.turnCounter = 0; saveConfig(ctx.cwd, config); ctx.ui.notify("🧠 Top of mind prompt updated and enabled", "info"); }, }); /** * /top-of-mind:file * Set a file whose contents are injected every turn. * With no arguments, shows current file path. */ pi.registerCommand("top-of-mind:file", { description: "Set a file whose contents are injected every turn. Usage: /top-of-mind:file or /top-of-mind:file to show current.", handler: async (args, ctx) => { if (!args || args.trim() === "") { ctx.ui.notify( `Current file: ${config.file ?? "(not set)"}`, "info", ); return; } const resolved = resolvePath(args.trim(), ctx.cwd); try { await access(resolved); config.file = args.trim(); config.enabled = true; config.turnCounter = 0; saveConfig(ctx.cwd, config); ctx.ui.notify("🧠 Top of mind file updated and enabled", "info"); } catch { ctx.ui.notify( `⚠️ File not found or not readable: ${resolved}`, "error", ); } }, }); /** * /top-of-mind:start * Enable top-of-mind injection. */ pi.registerCommand("top-of-mind:start", { description: "Enable top-of-mind injection.", handler: async (_args, ctx) => { if (!hasContent(config)) { ctx.ui.notify( "⚠️ No top-of-mind prompt or file configured. Use /top-of-mind:prompt or /top-of-mind:file first.", "warning", ); return; } config.enabled = true; config.turnCounter = 0; saveConfig(ctx.cwd, config); ctx.ui.notify("🧠 Top of mind enabled", "info"); }, }); /** * /top-of-mind:stop * Disable top-of-mind injection. */ pi.registerCommand("top-of-mind:stop", { description: "Disable top-of-mind injection.", handler: async (_args, ctx) => { config.enabled = false; saveConfig(ctx.cwd, config); ctx.ui.notify("🧠 Top of mind disabled", "info"); }, }); /** * /top-of-mind:frequency * Set how many turns between injections. With no arguments, shows current frequency. */ pi.registerCommand("top-of-mind:frequency", { description: "Set injection frequency (turns between injections). Usage: /top-of-mind:frequency or /top-of-mind:frequency to show current.", handler: async (args, ctx) => { if (!args || args.trim() === "") { ctx.ui.notify( `Current frequency: every ${config.frequency} turn(s)`, "info", ); return; } const parsed = parseInt(args.trim(), 10); if (isNaN(parsed) || parsed < 1) { ctx.ui.notify( `⚠️ Invalid frequency: "${args.trim()}". Must be a positive integer.`, "error", ); return; } config.frequency = parsed; config.turnCounter = 0; saveConfig(ctx.cwd, config); ctx.ui.notify( `🧠 Top of mind frequency set to every ${parsed} turn(s)`, "info", ); }, }); /** * /top-of-mind:status * Show current top-of-mind status. */ pi.registerCommand("top-of-mind:status", { description: "Show current top-of-mind configuration and status.", handler: async (_args, ctx) => { const lines = [ `🧠 Top of Mind Status`, ` Enabled: ${config.enabled ? "✅ yes" : "❌ no"}`, ` Prompt: ${config.text ? `"${config.text.slice(0, 80)}${config.text.length > 80 ? "..." : ""}"` : "(not set)"}`, ` File: ${config.file ?? "(not set)"}`, ` Frequency: every ${config.frequency} turn(s)`, ` Counter: ${config.turnCounter}`, ]; ctx.ui.notify(lines.join("\n"), "info"); }, }); }