import { randomUUID } from "node:crypto"; import { basename } from "node:path"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; const MAX_INSTANCE_LENGTH = 56; const MAX_SUMMARY_LENGTH = 90; const MAX_TURN_CONTEXT = 1200; function compact(text: string): string { return text.replace(/```[\s\S]*?```/g, " ").replace(/\s+/g, " ").trim(); } function truncate(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`; } function safeSummary(summary: string): string | undefined { const cleaned = compact(summary) .replace(/^['"`]|['"`]$/g, "") .replace(/https?:\/\/\S+/g, "link"); if (!cleaned || cleaned === "NO_CHANGE") return undefined; // Avoid putting likely credentials or secret material on the lock screen. if (/password|contraseña|api[_ -]?key|secret|token|private key|seed phrase|pass:/i.test(cleaned)) { return undefined; } return truncate(cleaned, MAX_SUMMARY_LENGTH); } function messageText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter( (block): block is { type: "text"; text: string } => Boolean(block) && typeof block === "object" && (block as { type?: unknown }).type === "text" && typeof (block as { text?: unknown }).text === "string", ) .map((block) => block.text) .join("\n"); } async function gitBranch(pi: ExtensionAPI, cwd: string): Promise { try { const result = await pi.exec("git", ["-C", cwd, "branch", "--show-current"], { timeout: 1500, }); const branch = result.stdout.trim(); return result.code === 0 && branch ? branch : undefined; } catch { return undefined; } } async function instanceLabel(pi: ExtensionAPI, ctx: ExtensionContext): Promise { const sessionName = pi.getSessionName()?.trim(); if (sessionName) return truncate(sessionName, MAX_INSTANCE_LENGTH); const branch = await gitBranch(pi, ctx.cwd); if (branch) return truncate(branch, MAX_INSTANCE_LENGTH); const sessionId = ctx.sessionManager.getSessionId(); const suffix = sessionId ? ` · ${sessionId.slice(0, 8)}` : ""; return truncate(`${basename(ctx.cwd)}${suffix}`, MAX_INSTANCE_LENGTH); } async function summarizeTurn( ctx: ExtensionContext, userPrompt: string, assistantResult: string, previousSummary?: string, ): Promise { if (!ctx.model || !ctx.modelRegistry.hasConfiguredAuth(ctx.model)) return undefined; const prompt = [ "Summarize this turn's work for a desktop notification.", "Return only one English phrase of at most 8 words.", "Describe the work completed or primary objective; do not quote the user's request.", "Use a concise noun phrase or an infinitive verb phrase.", "If the turn was only a test, greeting, thanks, acknowledgement, or a question about whether notifications work, return exactly NO_CHANGE.", "Do not include credentials, URLs, addresses, people's names, or sensitive data.", `Previous work summary: ${previousSummary ?? "none"}`, `Request: ${truncate(compact(userPrompt), MAX_TURN_CONTEXT)}`, `Result: ${truncate(compact(assistantResult), MAX_TURN_CONTEXT)}`, ].join("\n"); try { const response = await ctx.modelRegistry.complete( ctx.model, { messages: [ { role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now(), }, ], }, { reasoningEffort: "minimal", cacheRetention: "none", sessionId: randomUUID(), }, ); const text = response.content .filter((block): block is { type: "text"; text: string } => block.type === "text") .map((block) => block.text) .join(" "); return safeSummary(text); } catch { return undefined; } } function sendTerminalNotification(title: string, body: string): void { // OSC 777 is supported by Ghostty and several other modern terminals. process.stdout.write(`\x1b]777;notify;${title};${body}\x07`); } async function sendDesktopNotification( pi: ExtensionAPI, title: string, body: string, ): Promise { if (process.platform !== "darwin") return; const script = `display notification ${JSON.stringify(body)} with title ${JSON.stringify(title)}`; await pi.exec("osascript", ["-e", script], { timeout: 5000 }); } async function notify(pi: ExtensionAPI, title: string, body: string): Promise { sendTerminalNotification(title, body); await sendDesktopNotification(pi, title, body); } export default function smartNotify(pi: ExtensionAPI) { let currentPrompt = ""; let lastAssistantResult = ""; let workSummary: string | undefined; pi.on("session_start", (_event, ctx) => { currentPrompt = ""; lastAssistantResult = ""; workSummary = undefined; for (const entry of ctx.sessionManager.getBranch()) { if (entry.type !== "custom" || entry.customType !== "smart-notify-summary-v2") continue; const data = entry.data as { summary?: string } | undefined; workSummary = data?.summary ? safeSummary(data.summary) : workSummary; } }); pi.on("before_agent_start", (event) => { currentPrompt = event.prompt; lastAssistantResult = ""; }); pi.on("message_end", (event) => { if (event.message.role !== "assistant") return; const text = messageText(event.message.content); if (text.trim()) lastAssistantResult = text; }); pi.on("agent_settled", async (_event, ctx) => { if (ctx.mode !== "tui") return; const generated = await summarizeTurn( ctx, currentPrompt, lastAssistantResult, workSummary, ); if (generated) { workSummary = generated; pi.appendEntry("smart-notify-summary-v2", { summary: generated }); } const instance = await instanceLabel(pi, ctx); const body = workSummary ? `${workSummary} — completed` : `Work completed: ${instance}`; await notify(pi, `Pi · ${instance}`, body); }); pi.registerCommand("notify-test", { description: "Test the instance-aware macOS completion notification", handler: async (_args, ctx) => { const instance = await instanceLabel(pi, ctx); const body = workSummary ? `${workSummary} — completed` : `Work completed: ${instance}`; await notify(pi, `Pi · ${instance}`, body); ctx.ui.notify("Notification sent", "info"); }, }); }