From 823af3c48636884a3dcb6dfb8b5c92e5d8694458 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 28 Apr 2026 13:02:18 +0100 Subject: [PATCH] feat(pi-notifications): switch to afplay audio instead of desktop notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .pi/llm-metrics.log | 5 ++ packages/pi-notifications/src/index.ts | 21 +++----- packages/pi-notifications/src/test-notify.ts | 56 +++++--------------- 3 files changed, 26 insertions(+), 56 deletions(-) diff --git a/.pi/llm-metrics.log b/.pi/llm-metrics.log index 100f32d..475e116 100644 --- a/.pi/llm-metrics.log +++ b/.pi/llm-metrics.log @@ -34,3 +34,8 @@ {"timestamp":"2026-04-28T11:26:02.822Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":260,"outputTokens":299,"totalTokens":559,"prefillTokensPerSec":349.46,"generationTokensPerSec":46.53,"combinedTokensPerSec":77.96,"totalDurationMs":7170,"timeToFirstTokenMs":744,"rawTimestamps":{"ttftMs":744,"allTtftMs":[744],"generationDurationMs":6426,"turns":[{"turnId":"turn-0","durationMs":1741},{"turnId":"turn-1","durationMs":5429,"ttftMs":744}]}} {"timestamp":"2026-04-28T11:28:33.270Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":23,"outputTokens":510,"totalTokens":533,"prefillTokensPerSec":3.54,"generationTokensPerSec":123.76,"combinedTokensPerSec":50.19,"totalDurationMs":10619,"timeToFirstTokenMs":6498,"rawTimestamps":{"ttftMs":6498,"allTtftMs":[6498],"generationDurationMs":4121,"turns":[{"turnId":"turn-0","durationMs":10619,"ttftMs":6498}]}} {"timestamp":"2026-04-28T11:29:17.677Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":33,"outputTokens":628,"totalTokens":661,"prefillTokensPerSec":3.86,"generationTokensPerSec":135.23,"combinedTokensPerSec":50.12,"totalDurationMs":13188,"timeToFirstTokenMs":8544,"rawTimestamps":{"ttftMs":8544,"allTtftMs":[8544],"generationDurationMs":4644,"turns":[{"turnId":"turn-0","durationMs":13188,"ttftMs":8544}]}} +{"timestamp":"2026-04-28T11:56:04.867Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":9,"inputTokens":3958,"outputTokens":3210,"totalTokens":7168,"prefillTokensPerSec":2122.25,"generationTokensPerSec":59.8,"combinedTokensPerSec":129.05,"totalDurationMs":55543,"timeToFirstTokenMs":1865,"rawTimestamps":{"ttftMs":1865,"allTtftMs":[1865,1166,1677,3834,957,729],"generationDurationMs":53678,"turns":[{"turnId":"turn-0","durationMs":2745},{"turnId":"turn-1","durationMs":10638},{"turnId":"turn-2","durationMs":7403,"ttftMs":1865},{"turnId":"turn-3","durationMs":6495,"ttftMs":1166},{"turnId":"turn-4","durationMs":4477,"ttftMs":1677},{"turnId":"turn-5","durationMs":14052,"ttftMs":3834},{"turnId":"turn-6","durationMs":3112},{"turnId":"turn-7","durationMs":4345,"ttftMs":957},{"turnId":"turn-8","durationMs":2276,"ttftMs":729}]}} +{"timestamp":"2026-04-28T11:58:52.177Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":14,"outputTokens":43,"totalTokens":57,"prefillTokensPerSec":18.79,"generationTokensPerSec":113.16,"combinedTokensPerSec":50.67,"totalDurationMs":1125,"timeToFirstTokenMs":745,"rawTimestamps":{"ttftMs":745,"allTtftMs":[745],"generationDurationMs":380,"turns":[{"turnId":"turn-0","durationMs":1125,"ttftMs":745}]}} +{"timestamp":"2026-04-28T11:59:02.465Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":16,"outputTokens":43,"totalTokens":59,"prefillTokensPerSec":23.49,"generationTokensPerSec":96.63,"combinedTokensPerSec":52.4,"totalDurationMs":1126,"timeToFirstTokenMs":681,"rawTimestamps":{"ttftMs":681,"allTtftMs":[681],"generationDurationMs":445,"turns":[{"turnId":"turn-0","durationMs":1126,"ttftMs":681}]}} +{"timestamp":"2026-04-28T12:00:27.499Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":362,"outputTokens":216,"totalTokens":578,"prefillTokensPerSec":231.16,"generationTokensPerSec":71.4,"combinedTokensPerSec":125.9,"totalDurationMs":4591,"timeToFirstTokenMs":1566,"rawTimestamps":{"ttftMs":1566,"allTtftMs":[1566],"generationDurationMs":3025,"turns":[{"turnId":"turn-0","durationMs":2349},{"turnId":"turn-1","durationMs":2242,"ttftMs":1566}]}} +{"timestamp":"2026-04-28T12:01:00.510Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":25,"outputTokens":361,"totalTokens":386,"prefillTokensPerSec":5.99,"generationTokensPerSec":98.55,"combinedTokensPerSec":49.24,"totalDurationMs":7839,"timeToFirstTokenMs":4176,"rawTimestamps":{"ttftMs":4176,"allTtftMs":[4176],"generationDurationMs":3663,"turns":[{"turnId":"turn-0","durationMs":7839,"ttftMs":4176}]}} diff --git a/packages/pi-notifications/src/index.ts b/packages/pi-notifications/src/index.ts index 25decf7..46601a7 100644 --- a/packages/pi-notifications/src/index.ts +++ b/packages/pi-notifications/src/index.ts @@ -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 } } diff --git a/packages/pi-notifications/src/test-notify.ts b/packages/pi-notifications/src/test-notify.ts index 9145e67..5898ce9 100644 --- a/packages/pi-notifications/src/test-notify.ts +++ b/packages/pi-notifications/src/test-notify.ts @@ -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); }