Commit 6727003a264e
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/notify.ts
@@ -0,0 +1,102 @@
+/**
+ * Pi Notify Extension
+ *
+ * Sends a native terminal/desktop notification when Pi agent is done and waiting for input.
+ * Supports multiple terminal protocols:
+ * - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
+ * - OSC 99: Kitty
+ * - notify-send: Linux desktop fallback (libnotify)
+ * - Windows toast: Windows Terminal (WSL)
+ *
+ * Based on: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/notify.ts
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+
+function windowsToastScript(title: string, body: string): string {
+ const type = "Windows.UI.Notifications";
+ const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
+ const template = `[${type}.ToastTemplateType]::ToastText01`;
+ const toast = `[${type}.ToastNotification]::new($xml)`;
+ return [
+ `${mgr} > $null`,
+ `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${body}')) > $null`,
+ `[${type}.ToastNotificationManager]::CreateToastNotifier('${title}').Show(${toast})`,
+ ].join("; ");
+}
+
+function notifyOSC777(title: string, body: string): void {
+ process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
+}
+
+function notifyOSC99(title: string, body: string): void {
+ // Kitty OSC 99: i=notification id, d=0 means not done yet, p=body for second part
+ process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
+ process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
+}
+
+function notifyWindows(title: string, body: string): void {
+ const { execFile } = require("child_process");
+ execFile("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
+}
+
+function notifyLinuxDesktop(title: string, body: string): void {
+ const { execFile } = require("child_process");
+ // Use notify-send (libnotify) for Linux desktop notifications
+ // -u low = low urgency, -t 5000 = 5 second timeout
+ execFile("notify-send", ["-u", "low", "-t", "5000", "-a", "pi", title, body], (err: Error | null) => {
+ if (err) {
+ // Fallback to OSC 777 if notify-send fails
+ notifyOSC777(title, body);
+ }
+ });
+}
+
+function detectTerminal(): "kitty" | "wsl" | "osc777" | "linux-desktop" {
+ // Windows Terminal (WSL)
+ if (process.env.WT_SESSION) {
+ return "wsl";
+ }
+ // Kitty terminal
+ if (process.env.KITTY_WINDOW_ID) {
+ return "kitty";
+ }
+ // Ghostty, WezTerm, iTerm2 - these support OSC 777
+ if (process.env.GHOSTTY_RESOURCES_DIR || process.env.WEZTERM_PANE || process.env.ITERM_SESSION_ID) {
+ return "osc777";
+ }
+ // Alacritty, foot, and other terminals that don't support OSC notifications
+ // Use notify-send on Linux
+ if (process.platform === "linux" && (process.env.ALACRITTY_WINDOW_ID || process.env.WAYLAND_DISPLAY || process.env.DISPLAY)) {
+ return "linux-desktop";
+ }
+ // Default fallback
+ return "osc777";
+}
+
+function notify(title: string, body: string): void {
+ const terminal = detectTerminal();
+
+ switch (terminal) {
+ case "wsl":
+ notifyWindows(title, body);
+ break;
+ case "kitty":
+ notifyOSC99(title, body);
+ break;
+ case "linux-desktop":
+ notifyLinuxDesktop(title, body);
+ break;
+ case "osc777":
+ default:
+ notifyOSC777(title, body);
+ break;
+ }
+}
+
+export default function (pi: ExtensionAPI) {
+ pi.on("agent_end", async () => {
+ notify("Pi", "Ready for input");
+ });
+}