/** * Notify Extension * * Sends a notification when Pi finishes a prompt or when interactive tools are called. * If on SSH/remote, SSHs back to Linux PC to play sound + show notification. */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; // ============ CONFIGURATION ============ const REMOTE_HOST = "linux-pc"; // SSH host for remote notifications const LOCAL_SOUND_MAC = "/System/Library/Sounds/Glass.aiff"; const LOCAL_SOUND_LINUX = "/run/current-system/sw/share/sounds/freedesktop/stereo/window-attention.oga"; // ======================================= function isSSH(): boolean { if ( process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY ) { return true; } // Check for sshd-session process (works in tmux/zellij) try { const { execSync } = require("child_process"); const result = execSync("pgrep -u $USER -f sshd-session 2>/dev/null", { encoding: "utf-8", timeout: 1000, }); return result.trim().length > 0; } catch { return false; } } function notifyRemote(message: string): void { const { exec } = require("child_process"); exec( `ssh -o ConnectTimeout=2 -o BatchMode=yes ${REMOTE_HOST} "pw-play /run/current-system/sw/share/sounds/freedesktop/stereo/window-attention.oga & notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' '${message}'"`, { timeout: 5000 }, ); } function notifyLocal(message: string): void { const { execFile, exec } = require("child_process"); if (process.platform === "darwin") { execFile("afplay", [LOCAL_SOUND_MAC]); } else if (process.platform === "linux") { execFile( "pw-play", [LOCAL_SOUND_LINUX], (err: any) => { if (err) console.error("pw-play error:", err); }, ); exec( `notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' '${message}'`, (err: any) => { if (err) console.error("notify-send error:", err); }, ); } } export function sendNotification(message: string = "Done. Ready for input."): void { if (process.platform === "darwin" || isSSH()) { notifyRemote(message); } // always notify locally too notifyLocal(message); } const ACTION_NOTIFY_TOOLS = new Set(["question", "questionnaire"]); export default function (pi: ExtensionAPI) { pi.on("agent_end", async (_event, ctx) => { if (!ctx.hasUI) return; sendNotification(); }); // Notify when interactive tools are called so the user notices prompts immediately pi.on("tool_call", async (event, ctx) => { if (!ctx.hasUI) return; if (!ACTION_NOTIFY_TOOLS.has(event.toolName)) return; sendNotification("Question requires input"); }); // Simple test command pi.registerCommand("notify", { description: "Test notification", handler: async (_args, ctx) => { sendNotification(); ctx.ui.notify("Notification sent!", "info"); }, }); }