From b1475466e28cf7ae23459dcaa4904215c3016f23 Mon Sep 17 00:00:00 2001 From: Liuyang Wan Date: Mon, 24 Aug 2026 11:29:54 +0800 Subject: [PATCH] fix: debounce permission notifications to avoid false positives in auto mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of swapping permission.updated for permission.asked (which fires under the same condition), debounce permission notifications by 500ms and cancel them if permission.replied arrives within that window. In auto mode, Warp auto-replies near-instantly → notification cancelled → no false 'agent blocked' notification. In non-auto mode, no instant reply arrives → notification fires after the grace period → user sees it as before. Both permission.updated (v1 SDK) and permission.asked (v2 SDK) are handled, preserving compatibility across opencode versions. --- src/index.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index c9ace13..27be4e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { truncate, extractTextFromParts } from "./utils" // is not a function ("Plugin export is not a function"). const PLUGIN_VERSION = "0.1.7" const NOTIFICATION_TITLE = "warp://cli-agent" +const PERMISSION_NOTIFY_DELAY_MS = 500 function buildPermissionPayload(perm: Permission, cwd: string) { const toolName = perm.type || "unknown" @@ -94,6 +95,38 @@ export const WarpPlugin: Plugin = async ({ client, directory }) => { warpNotify(NOTIFICATION_TITLE, body) } + // Debounce permission notifications: if a permission.replied arrives + // within the grace period (e.g. Warp auto-approved in auto mode), the + // notification is cancelled. This avoids false "agent blocked" + // notifications while preserving them for genuine user prompts. + const pendingPermissionNotify = new Map>() + + function schedulePermissionNotify( + requestId: string | undefined, + sessionId: string | undefined, + body: string, + ): void { + if (!requestId) { + void maybeWarpNotify(sessionId, body) + return + } + if (pendingPermissionNotify.has(requestId)) return + const timer = setTimeout(() => { + pendingPermissionNotify.delete(requestId) + void maybeWarpNotify(sessionId, body) + }, PERMISSION_NOTIFY_DELAY_MS) + pendingPermissionNotify.set(requestId, timer) + } + + function cancelPermissionNotify(requestId: string | undefined): void { + if (!requestId) return + const timer = pendingPermissionNotify.get(requestId) + if (timer) { + clearTimeout(timer) + pendingPermissionNotify.delete(requestId) + } + } + return { event: async ({ event }: { event: Event }) => { const cwd = directory || "" @@ -156,7 +189,8 @@ export const WarpPlugin: Plugin = async ({ client, directory }) => { } case "permission.updated": { - await maybeWarpNotify( + schedulePermissionNotify( + event.properties.id, event.properties.sessionID, buildPermissionPayload(event.properties, cwd), ) @@ -164,20 +198,31 @@ export const WarpPlugin: Plugin = async ({ client, directory }) => { } case "permission.replied": { - const { sessionID, response } = event.properties + const props = event.properties as { + sessionID: string + response?: string + reply?: string + permissionID?: string + requestID?: string + } + cancelPermissionNotify(props.requestID ?? props.permissionID) + const response = props.response ?? props.reply if (response === "reject") return - const body = buildPayload("permission_replied", sessionID, cwd) - await maybeWarpNotify(sessionID, body) + const body = buildPayload("permission_replied", props.sessionID, cwd) + await maybeWarpNotify(props.sessionID, body) return } default: { - // permission.asked is listed in the opencode docs but has no SDK type. - // Handle it with the same logic as permission.updated. + // permission.asked exists in the v2 SDK but not v1; + // handle it via the default case with the same debounce + // logic as permission.updated. if ((event as any).type === "permission.asked") { - await maybeWarpNotify( - (event as any).properties?.sessionID, - buildPermissionPayload((event as any).properties, cwd), + const props = (event as any).properties + schedulePermissionNotify( + props?.id, + props?.sessionID, + buildPermissionPayload(props, cwd), ) } }