Commit 6727003a264e

Vincent Demeester <vincent@sbr.pm>
2026-02-04 16:15:25
pi/extensions: add notify.ts for desktop notifications
Sends native terminal/desktop notifications when pi agent finishes and awaits input. Supports multiple terminal protocols: - OSC 777: Ghostty, WezTerm, iTerm2 - OSC 99: Kitty - notify-send: Linux desktop fallback (Alacritty, foot, etc.) - Windows toast: WSL Based on badlogic/pi-mono notify extension with Linux enhancements. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 82f9b45
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");
+	});
+}