feat(pi-notifications): switch to afplay audio instead of desktop notifications
- Uses afplay to play an audio file (default: Glass.aiff) - Configurable via PI_NOTIFICATION_AUDIO env var - Works from sandboxed context — no osascript needed - test-notify.ts verifies audio playback standalone - Synced to auto-discovery extension path
This commit is contained in:
@@ -1,31 +1,24 @@
|
||||
// Desktop notifications for pi agent events
|
||||
// Uses osascript (macOS) to trigger Notification Center alerts
|
||||
// Plays an audio file to alert the user
|
||||
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
// Configuration via environment variables
|
||||
const enabled = process.env.PI_NOTIFICATIONS_ENABLED !== "false";
|
||||
const agentEndEnabled = process.env.PI_NOTIFICATION_AGENT_END !== "false";
|
||||
const debug = process.env.PI_NOTIFICATION_DEBUG === "true";
|
||||
const title = process.env.PI_NOTIFICATION_TITLE || "pi";
|
||||
const sound = process.env.PI_NOTIFICATION_SOUND || "default";
|
||||
const targetApp = process.env.PI_NOTIFICATION_APP || "Ghostty";
|
||||
const audioPath = process.env.PI_NOTIFICATION_AUDIO || "/System/Library/Sounds/Glass.aiff";
|
||||
|
||||
function notify(body: string, subtitle?: string): void {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
const sub = subtitle ? `subtitle "${subtitle}"` : "";
|
||||
// "default" is a reserved word in AppleScript, so only add sound param if it's a custom sound
|
||||
const snd = sound && sound !== "default" ? `sound "${sound}"` : "";
|
||||
// Tell the target app to display the notification — avoids the "Show" button
|
||||
// that appears when osascript is the attributed app
|
||||
execSync(
|
||||
`osascript -e 'tell application "${targetApp}" to display notification "${body}" with title "${title}" ${sub} ${snd}'`.trim(),
|
||||
{ stdio: "ignore" }
|
||||
);
|
||||
if (existsSync(audioPath)) {
|
||||
execSync(`afplay "${audioPath}"`, { stdio: "ignore" });
|
||||
}
|
||||
} catch {
|
||||
// osascript not available (non-macOS) — silently fail
|
||||
// audio playback failed — silently fail
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +1,24 @@
|
||||
// Standalone notification tester — run from bash to verify osascript works
|
||||
// Standalone audio tester — run from bash to verify audio playback works
|
||||
// Usage: npx jiti packages/pi-notifications/src/test-notify.ts
|
||||
//
|
||||
// This is completely decoupled from the agent loop.
|
||||
// Use it to verify that the extension's notification machinery works
|
||||
// before debugging event handler wiring.
|
||||
// Use it to verify that audio playback works before debugging event handler wiring.
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const title = process.env.PI_NOTIFICATION_TITLE || "pi";
|
||||
const sound = process.env.PI_NOTIFICATION_SOUND || "default";
|
||||
const targetApp = process.env.PI_NOTIFICATION_APP || "Ghostty";
|
||||
const audioPath = process.env.PI_NOTIFICATION_AUDIO || "/System/Library/Sounds/Glass.aiff";
|
||||
|
||||
// "default" is a reserved word in AppleScript, so only add sound param if it's a custom sound
|
||||
const soundArg = sound && sound !== "default" ? `sound "${sound}"` : "";
|
||||
|
||||
function tryNotify(tellApp: string): string {
|
||||
const cmd = `osascript -e 'tell application "${tellApp}" to display notification "Test notification from pi-notifications" with title "${title}" ${soundArg}'`.trim();
|
||||
return cmd;
|
||||
}
|
||||
|
||||
function tryPlainNotify(): string {
|
||||
const cmd = `osascript -e 'display notification "Test notification from pi-notifications" with title "${title}" ${soundArg}'`.trim();
|
||||
return cmd;
|
||||
}
|
||||
|
||||
let cmd = tryNotify(targetApp);
|
||||
let success = false;
|
||||
let lastError: string | undefined;
|
||||
|
||||
// Try telling the target app first, fall back to plain display notification
|
||||
for (const attempt of [
|
||||
{ cmd: tryNotify(targetApp), label: `tell "${targetApp}"` },
|
||||
{ cmd: tryPlainNotify(), label: "plain" },
|
||||
]) {
|
||||
try {
|
||||
console.log(`[test-notify] trying ${attempt.label}:`, attempt.cmd);
|
||||
execSync(attempt.cmd, { stdio: ["ignore", "pipe", "pipe"] });
|
||||
success = true;
|
||||
break;
|
||||
} catch (e: any) {
|
||||
lastError = e.message;
|
||||
console.log(`[test-notify] ${attempt.label} failed:`, e.message.split("\n")[0]);
|
||||
try {
|
||||
if (!existsSync(audioPath)) {
|
||||
console.error("[test-audio] ❌ Audio file not found:", audioPath);
|
||||
console.error("[test-audio] Set PI_NOTIFICATION_AUDIO to a valid .aiff/.wav/.mp3 path");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
console.log("[test-notify] ✅ Notification sent — check your Notification Center");
|
||||
} else {
|
||||
console.error("[test-notify] ❌ Failed:", lastError);
|
||||
console.log("[test-audio] playing:", audioPath);
|
||||
execSync(`afplay "${audioPath}"`, { stdio: ["ignore", "pipe", "pipe"] });
|
||||
console.log("[test-audio] ✅ Audio played");
|
||||
} catch (e: any) {
|
||||
console.error("[test-audio] ❌ Failed:", e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user