From 040513e1d6c1de9a17aa933870766849626d1dbb Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Tue, 28 Apr 2026 12:56:02 +0100 Subject: [PATCH] feat(pi-notifications): use 'tell application' for notifications to suppress Show button - Tell target app (default: Ghostty) to display notification instead of raw osascript - This attributes notification to the app, avoiding the 'Show' button that opens Script Editor - Configurable via PI_NOTIFICATION_APP env var - test-notify.ts falls back to plain display notification if target app isn't running - Synced to auto-discovery extension path --- .pi/llm-metrics.log | 9 +++++ packages/pi-notifications/src/index.ts | 5 ++- packages/pi-notifications/src/test-notify.ts | 39 +++++++++++++++++--- plans/pi-notifications-version-0.md | 18 +++++++-- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/.pi/llm-metrics.log b/.pi/llm-metrics.log index 9437ab7..100f32d 100644 --- a/.pi/llm-metrics.log +++ b/.pi/llm-metrics.log @@ -25,3 +25,12 @@ {"timestamp":"2026-04-28T11:04:07.153Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":14,"outputTokens":20,"totalTokens":34,"prefillTokensPerSec":11.77,"generationTokensPerSec":176.99,"combinedTokensPerSec":26.11,"totalDurationMs":1302,"timeToFirstTokenMs":1189,"rawTimestamps":{"ttftMs":1189,"allTtftMs":[1189],"generationDurationMs":113,"turns":[{"turnId":"turn-0","durationMs":1302,"ttftMs":1189}]}} {"timestamp":"2026-04-28T11:06:19.732Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":3955,"outputTokens":52,"totalTokens":4007,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":823.13,"totalDurationMs":4868,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":4868}]}} {"timestamp":"2026-04-28T11:07:01.680Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":1592,"outputTokens":977,"totalTokens":2569,"prefillTokensPerSec":486.11,"generationTokensPerSec":59.49,"combinedTokensPerSec":130.41,"totalDurationMs":19699,"timeToFirstTokenMs":3275,"rawTimestamps":{"ttftMs":3275,"allTtftMs":[3275,3442],"generationDurationMs":16424,"turns":[{"turnId":"turn-0","durationMs":1422},{"turnId":"turn-1","durationMs":5574,"ttftMs":3275},{"turnId":"turn-2","durationMs":12703,"ttftMs":3442}]}} +{"timestamp":"2026-04-28T11:10:37.283Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":32,"inputTokens":6935,"outputTokens":7163,"totalTokens":14098,"prefillTokensPerSec":10428.57,"generationTokensPerSec":50.87,"combinedTokensPerSec":99.66,"totalDurationMs":141464,"timeToFirstTokenMs":665,"rawTimestamps":{"ttftMs":665,"allTtftMs":[665,2503,870,1306,941,835,963,4510,903,972,803],"generationDurationMs":140799,"turns":[{"turnId":"turn-0","durationMs":2679},{"turnId":"turn-1","durationMs":6615},{"turnId":"turn-2","durationMs":6542,"ttftMs":665},{"turnId":"turn-3","durationMs":4622},{"turnId":"turn-4","durationMs":13602,"ttftMs":2503},{"turnId":"turn-5","durationMs":1926,"ttftMs":870},{"turnId":"turn-6","durationMs":8320},{"turnId":"turn-7","durationMs":2635,"ttftMs":1306},{"turnId":"turn-8","durationMs":3534,"ttftMs":941},{"turnId":"turn-9","durationMs":7422,"ttftMs":835},{"turnId":"turn-10","durationMs":2988,"ttftMs":963},{"turnId":"turn-11","durationMs":2253},{"turnId":"turn-12","durationMs":2253},{"turnId":"turn-13","durationMs":2162},{"turnId":"turn-14","durationMs":2460},{"turnId":"turn-15","durationMs":1826},{"turnId":"turn-16","durationMs":1966},{"turnId":"turn-17","durationMs":2357},{"turnId":"turn-18","durationMs":2036},{"turnId":"turn-19","durationMs":3039},{"turnId":"turn-20","durationMs":3115},{"turnId":"turn-21","durationMs":2912},{"turnId":"turn-22","durationMs":3526},{"turnId":"turn-23","durationMs":11560,"ttftMs":4510},{"turnId":"turn-24","durationMs":9433},{"turnId":"turn-25","durationMs":1627},{"turnId":"turn-26","durationMs":5835},{"turnId":"turn-27","durationMs":2414},{"turnId":"turn-28","durationMs":2242,"ttftMs":903},{"turnId":"turn-29","durationMs":5854},{"turnId":"turn-30","durationMs":4553,"ttftMs":972},{"turnId":"turn-31","durationMs":7156,"ttftMs":803}]}} +{"timestamp":"2026-04-28T11:16:13.226Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":21,"outputTokens":44,"totalTokens":65,"prefillTokensPerSec":24.91,"generationTokensPerSec":102.8,"combinedTokensPerSec":51.14,"totalDurationMs":1271,"timeToFirstTokenMs":843,"rawTimestamps":{"ttftMs":843,"allTtftMs":[843],"generationDurationMs":428,"turns":[{"turnId":"turn-0","durationMs":1271,"ttftMs":843}]}} +{"timestamp":"2026-04-28T11:18:27.997Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":94,"outputTokens":699,"totalTokens":793,"prefillTokensPerSec":17.94,"generationTokensPerSec":77.6,"combinedTokensPerSec":55.66,"totalDurationMs":14248,"timeToFirstTokenMs":5240,"rawTimestamps":{"ttftMs":5240,"allTtftMs":[5240,1223],"generationDurationMs":9008,"turns":[{"turnId":"turn-0","durationMs":11689,"ttftMs":5240},{"turnId":"turn-1","durationMs":2559,"ttftMs":1223}]}} +{"timestamp":"2026-04-28T11:19:48.781Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":513,"outputTokens":501,"totalTokens":1014,"prefillTokensPerSec":335.08,"generationTokensPerSec":64.65,"combinedTokensPerSec":109.26,"totalDurationMs":9281,"timeToFirstTokenMs":1531,"rawTimestamps":{"ttftMs":1531,"allTtftMs":[1531,1219],"generationDurationMs":7750,"turns":[{"turnId":"turn-0","durationMs":7110,"ttftMs":1531},{"turnId":"turn-1","durationMs":2171,"ttftMs":1219}]}} +{"timestamp":"2026-04-28T11:20:48.239Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":19,"outputTokens":50,"totalTokens":69,"prefillTokensPerSec":26.24,"generationTokensPerSec":124.38,"combinedTokensPerSec":61.28,"totalDurationMs":1126,"timeToFirstTokenMs":724,"rawTimestamps":{"ttftMs":724,"allTtftMs":[724],"generationDurationMs":402,"turns":[{"turnId":"turn-0","durationMs":1126,"ttftMs":724}]}} +{"timestamp":"2026-04-28T11:24:12.631Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":4,"inputTokens":3761,"outputTokens":1175,"totalTokens":4936,"prefillTokensPerSec":495.13,"generationTokensPerSec":62.4,"combinedTokensPerSec":186.79,"totalDurationMs":26425,"timeToFirstTokenMs":7596,"rawTimestamps":{"ttftMs":7596,"allTtftMs":[7596,724],"generationDurationMs":18829,"turns":[{"turnId":"turn-0","durationMs":2202},{"turnId":"turn-1","durationMs":6207},{"turnId":"turn-2","durationMs":15923,"ttftMs":7596},{"turnId":"turn-3","durationMs":2093,"ttftMs":724}]}} +{"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}]}} diff --git a/packages/pi-notifications/src/index.ts b/packages/pi-notifications/src/index.ts index 38ab608..25decf7 100644 --- a/packages/pi-notifications/src/index.ts +++ b/packages/pi-notifications/src/index.ts @@ -10,6 +10,7 @@ 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"; function notify(body: string, subtitle?: string): void { if (!enabled) return; @@ -17,8 +18,10 @@ function notify(body: string, subtitle?: string): void { 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 'display notification "${body}" with title "${title}" ${sub} ${snd}'`.trim(), + `osascript -e 'tell application "${targetApp}" to display notification "${body}" with title "${title}" ${sub} ${snd}'`.trim(), { stdio: "ignore" } ); } catch { diff --git a/packages/pi-notifications/src/test-notify.ts b/packages/pi-notifications/src/test-notify.ts index 6d320b7..9145e67 100644 --- a/packages/pi-notifications/src/test-notify.ts +++ b/packages/pi-notifications/src/test-notify.ts @@ -9,17 +9,44 @@ import { execSync } from "node:child_process"; const title = process.env.PI_NOTIFICATION_TITLE || "pi"; const sound = process.env.PI_NOTIFICATION_SOUND || "default"; +const targetApp = process.env.PI_NOTIFICATION_APP || "Ghostty"; // "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}"` : ""; -try { +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(); - console.log("[test-notify] running:", cmd); - execSync(cmd, { stdio: ["ignore", "pipe", "pipe"] }); + 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]); + } +} + +if (success) { console.log("[test-notify] ✅ Notification sent — check your Notification Center"); -} catch (e: any) { - console.error("[test-notify] ❌ Failed:", e.message); - console.error("[test-notify] Try running the command directly in bash to verify osascript works."); +} else { + console.error("[test-notify] ❌ Failed:", lastError); process.exit(1); } diff --git a/plans/pi-notifications-version-0.md b/plans/pi-notifications-version-0.md index 84ec6fb..fc99131 100644 --- a/plans/pi-notifications-version-0.md +++ b/plans/pi-notifications-version-0.md @@ -42,10 +42,20 @@ If `execSync` fails silently, try: ### 2. Check macOS notification settings -The notification might be suppressed by macOS settings: -- System Settings → Notifications → check that "Terminal" or "pi" is allowed -- Check if "Show Notifications on Lock Screen" is enabled -- Check if "Focus Modes" are suppressing notifications +Notifications may be delivered but not shown as banners: +- **System Settings → Notifications → Ghostty → Notification Style** — must be "Banners" or "Alerts", not "None" (osascript fires from the Ghostty process, so macOS attributes notifications to Ghostty, not "pi") +- **System Settings → Focus → [active focus] → Apps** — ensure "Ghostty" is not excluded +- **System Settings → Notifications → Show Notifications on Lock Screen** — enable if needed + +**Known symptom:** Notifications appear in Notification Center when pulled down, but never pop up as banners. This is a macOS style setting, not a code issue. + +### 3. Ghostty suppresses banners when focused + +Ghostty intentionally silences banner notifications (no pop-up, no sound) when the Ghostty window is **active/focused**. The notification is still delivered to Notification Center. Banners only appear when Ghostty is **not** the active window. + +**Workarounds:** +- **System Settings → Notifications → Ghostty → Alert Style → "Persistent"** — macOS shows these as banners regardless of Ghostty's silencing +- **Switch to another app** (e.g. leave your browser open) when you want to see the banner ### 3. Try alternative notification methods