better notifs

This commit is contained in:
2026-03-04 21:14:36 +00:00
parent d95cf0699c
commit c8a646ba3e
3 changed files with 16 additions and 12 deletions
@@ -6,12 +6,14 @@
*/ */
import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI, SessionBeforeSwitchEvent, SessionMessageEntry } from "@mariozechner/pi-coding-agent";
import { sendNotification } from "./notify.js";
export default function (pi: ExtensionAPI) { export default function (pi: ExtensionAPI) {
pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => { pi.on("session_before_switch", async (event: SessionBeforeSwitchEvent, ctx) => {
if (!ctx.hasUI) return; if (!ctx.hasUI) return;
if (event.reason === "new") { if (event.reason === "new") {
sendNotification("Clear session confirmation");
const confirmed = await ctx.ui.confirm( const confirmed = await ctx.ui.confirm(
"Clear session?", "Clear session?",
"This will delete all messages in the current session.", "This will delete all messages in the current session.",
@@ -31,6 +33,7 @@ export default function (pi: ExtensionAPI) {
); );
if (hasUnsavedWork) { if (hasUnsavedWork) {
sendNotification("Switch session confirmation");
const confirmed = await ctx.ui.confirm( const confirmed = await ctx.ui.confirm(
"Switch session?", "Switch session?",
"You have messages in the current session. Switch anyway?", "You have messages in the current session. Switch anyway?",
@@ -46,6 +49,7 @@ export default function (pi: ExtensionAPI) {
pi.on("session_before_fork", async (event, ctx) => { pi.on("session_before_fork", async (event, ctx) => {
if (!ctx.hasUI) return; if (!ctx.hasUI) return;
sendNotification("Fork session confirmation");
const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [ const choice = await ctx.ui.select(`Fork from entry ${event.entryId.slice(0, 8)}?`, [
"Yes, create fork", "Yes, create fork",
"No, stay in current session", "No, stay in current session",
@@ -1,7 +1,7 @@
/** /**
* Pi Done Notify Extension * Notify Extension
* *
* Sends a notification when Pi finishes a prompt. * Sends a notification when Pi finishes a prompt or when interactive tools are called.
* If on SSH/remote, SSHs back to Linux PC to play sound + show notification. * If on SSH/remote, SSHs back to Linux PC to play sound + show notification.
*/ */
@@ -9,8 +9,6 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
// ============ CONFIGURATION ============ // ============ CONFIGURATION ============
const REMOTE_HOST = "linux-pc"; // SSH host for remote notifications const REMOTE_HOST = "linux-pc"; // SSH host for remote notifications
const REMOTE_COMMAND =
"paplay /usr/share/sounds/freedesktop/stereo/window-attention.oga & notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' 'Done. Ready for input.'";
const LOCAL_SOUND_MAC = "/System/Library/Sounds/Glass.aiff"; const LOCAL_SOUND_MAC = "/System/Library/Sounds/Glass.aiff";
const LOCAL_SOUND_LINUX = "/usr/share/sounds/freedesktop/stereo/complete.oga"; const LOCAL_SOUND_LINUX = "/usr/share/sounds/freedesktop/stereo/complete.oga";
// ======================================= // =======================================
@@ -36,15 +34,15 @@ function isSSH(): boolean {
} }
} }
function notifyRemote(): void { function notifyRemote(message: string): void {
const { exec } = require("child_process"); const { exec } = require("child_process");
exec( exec(
`ssh -o ConnectTimeout=2 -o BatchMode=yes ${REMOTE_HOST} "${REMOTE_COMMAND}"`, `ssh -o ConnectTimeout=2 -o BatchMode=yes ${REMOTE_HOST} "paplay /usr/share/sounds/freedesktop/stereo/window-attention.oga & notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' '${message}'"`,
{ timeout: 5000 }, { timeout: 5000 },
); );
} }
function notifyLocal(): void { function notifyLocal(message: string): void {
const { execFile, exec } = require("child_process"); const { execFile, exec } = require("child_process");
if (process.platform === "darwin") { if (process.platform === "darwin") {
execFile("afplay", [LOCAL_SOUND_MAC]); execFile("afplay", [LOCAL_SOUND_MAC]);
@@ -57,7 +55,7 @@ function notifyLocal(): void {
}, },
); );
exec( exec(
"notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' 'Done. Ready for input.'", `notify-send -i ~/.pi/agent/extensions/assets/pi-logo.svg 'Pi' '${message}'`,
(err: any) => { (err: any) => {
if (err) console.error("notify-send error:", err); if (err) console.error("notify-send error:", err);
}, },
@@ -65,13 +63,13 @@ function notifyLocal(): void {
} }
} }
function sendNotification(): void { export function sendNotification(message: string = "Done. Ready for input."): void {
if (process.platform === "darwin" || isSSH()) { if (process.platform === "darwin" || isSSH()) {
notifyRemote(); notifyRemote(message);
} }
// always notify locally too // always notify locally too
notifyLocal(); notifyLocal(message);
} }
const ACTION_NOTIFY_TOOLS = new Set(["question", "questionnaire"]); const ACTION_NOTIFY_TOOLS = new Set(["question", "questionnaire"]);
@@ -86,7 +84,7 @@ export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => { pi.on("tool_call", async (event, ctx) => {
if (!ctx.hasUI) return; if (!ctx.hasUI) return;
if (!ACTION_NOTIFY_TOOLS.has(event.toolName)) return; if (!ACTION_NOTIFY_TOOLS.has(event.toolName)) return;
sendNotification(); sendNotification("Question requires input");
}); });
// Simple test command // Simple test command
@@ -6,6 +6,7 @@
*/ */
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { sendNotification } from "./notify.js";
export default function (pi: ExtensionAPI) { export default function (pi: ExtensionAPI) {
const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i]; const dangerousPatterns = [/\brm\s+(-rf?|--recursive)/i, /\bsudo\b/i, /\b(chmod|chown)\b.*777/i];
@@ -22,6 +23,7 @@ export default function (pi: ExtensionAPI) {
return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" }; return { block: true, reason: "Dangerous command blocked (no UI for confirmation)" };
} }
sendNotification("Destructive command pending");
const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]); const choice = await ctx.ui.select(`⚠️ Dangerous command:\n\n ${command}\n\nAllow?`, ["Yes", "No"]);
if (choice !== "Yes") { if (choice !== "Yes") {