feat(pi-notify): notify on permission prompts

This commit is contained in:
云服务部-叶林立
2026-08-28 16:35:33 +08:00
parent b83413d670
commit 8002d7c213
6 changed files with 157 additions and 33 deletions
+62 -5
View File
@@ -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;