mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-notify): notify on permission prompts
This commit is contained in:
+62
-5
@@ -1,6 +1,8 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { getConfig, shouldNotify } from "./config.ts";
|
||||
import { truncateText } from "./messages.ts";
|
||||
import { parseMessageMax, parseMessageSource } from "./parsers.ts";
|
||||
import { getNotificationBackend, sendNotification } from "./terminal.ts";
|
||||
import type { NotifyConfig } from "./types.ts";
|
||||
|
||||
@@ -11,6 +13,35 @@ type ExtensionDependencies = {
|
||||
getBackend: typeof getNotificationBackend;
|
||||
};
|
||||
|
||||
const PERMISSIONS_UI_PROMPT_CHANNEL = "permissions:ui_prompt";
|
||||
|
||||
function formatPermissionPromptBody(raw: unknown): string | undefined {
|
||||
if (!isRecord(raw) || !readNonEmptyString(raw.requestId)) return undefined;
|
||||
if (parseMessageSource(process.env.PI_NOTIFY_MESSAGE_SOURCE) === "none") {
|
||||
return "Permission confirmation required";
|
||||
}
|
||||
|
||||
const surface = readNonEmptyString(raw.surface);
|
||||
const value = readNonEmptyString(raw.value);
|
||||
const forwarding = isRecord(raw.forwarding) ? raw.forwarding : undefined;
|
||||
const agentName = readNonEmptyString(forwarding?.requesterAgentName) ?? readNonEmptyString(raw.agentName);
|
||||
const labels = ["Permission required", agentName, surface].filter((value): value is string => Boolean(value));
|
||||
|
||||
if (!value) return labels.join(" · ");
|
||||
const normalizedValue = value.replace(/\s+/g, " ").trim();
|
||||
return `${labels.join(" · ")}\n${truncateText(normalizedValue, parseMessageMax(process.env.PI_NOTIFY_MESSAGE_MAX))}`;
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
export function createPiNotifyExtension(dependencies: Partial<ExtensionDependencies> = {}) {
|
||||
const now = dependencies.now ?? Date.now;
|
||||
const createNotificationId = dependencies.createNotificationId ?? (() => `pi-${process.pid}-${randomUUID()}`);
|
||||
@@ -23,6 +54,7 @@ export function createPiNotifyExtension(dependencies: Partial<ExtensionDependenc
|
||||
let latestMessages: unknown[] | undefined;
|
||||
let toolErrorCount = 0;
|
||||
let enabledOverride: boolean | undefined;
|
||||
let currentCtx: ExtensionContext | undefined;
|
||||
|
||||
const resetRun = (): void => {
|
||||
startedAt = undefined;
|
||||
@@ -30,7 +62,12 @@ export function createPiNotifyExtension(dependencies: Partial<ExtensionDependenc
|
||||
toolErrorCount = 0;
|
||||
};
|
||||
|
||||
pi.on("agent_start", () => {
|
||||
pi.on("session_start", (_event, ctx) => {
|
||||
currentCtx = ctx;
|
||||
});
|
||||
|
||||
pi.on("agent_start", (_event, ctx) => {
|
||||
currentCtx = ctx;
|
||||
startedAt ??= now();
|
||||
});
|
||||
|
||||
@@ -51,14 +88,34 @@ export function createPiNotifyExtension(dependencies: Partial<ExtensionDependenc
|
||||
}
|
||||
});
|
||||
|
||||
const unsubscribePermissionPrompt = pi.events.on(PERMISSIONS_UI_PROMPT_CHANNEL, (raw) => {
|
||||
const body = formatPermissionPromptBody(raw);
|
||||
if (!body || currentCtx?.mode !== "tui") return;
|
||||
|
||||
const config = getConfig(
|
||||
currentCtx,
|
||||
undefined,
|
||||
undefined,
|
||||
{ notificationId, toolErrorCount },
|
||||
now(),
|
||||
);
|
||||
const permissionConfig = { ...config, body, minDurationMs: 0 };
|
||||
if (shouldNotify(permissionConfig, enabledOverride)) notify(permissionConfig);
|
||||
});
|
||||
|
||||
pi.on("session_shutdown", () => {
|
||||
currentCtx = undefined;
|
||||
unsubscribePermissionPrompt();
|
||||
});
|
||||
|
||||
pi.registerCommand("notify", {
|
||||
description: "Control Kitty/macOS completion notifications: on, off, test, status",
|
||||
description: "Control Kitty/macOS completion and permission notifications: on, off, test, status",
|
||||
handler: async (args, ctx) => {
|
||||
const action = args.trim().toLowerCase() || "status";
|
||||
|
||||
if (action === "on" || action === "off") {
|
||||
enabledOverride = action === "on";
|
||||
ctx.ui.notify(`Completion notifications ${action}`, "info");
|
||||
ctx.ui.notify(`Notifications ${action}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,7 +133,7 @@ export function createPiNotifyExtension(dependencies: Partial<ExtensionDependenc
|
||||
if (action === "status") {
|
||||
const enabled = enabledOverride ?? config.enabled;
|
||||
ctx.ui.notify(
|
||||
`Notifications ${enabled ? "on" : "off"} · ${getBackend()} · ${config.visibility} · min ${config.minDurationMs / 1000}s`,
|
||||
`Notifications ${enabled ? "on" : "off"} · ${getBackend()} · ${config.visibility} · completion min ${config.minDurationMs / 1000}s`,
|
||||
"info",
|
||||
);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user