From 0d74619241b470565c43f16bdd5eb9a90cf068a6 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 21 Mar 2026 13:34:03 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20OS=20notification=20lifecycle=20?= =?UTF-8?q?=E2=80=94=20dismiss=20on=20resolve,=20respect=20snooze?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Track notifications by requestId (Map instead of Set) so they can be dismissed individually when the approval is resolved - Dismiss OS notification in respondToToolApproval (via UI or action) - Dismiss OS notification on timeout auto-resolve - Dismiss all OS notifications on cleanupRun (team stop/exit) - Respect snoozedUntil — skip notification if user snoozed all --- .../services/team/TeamProvisioningService.ts | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 25fcd46e..e4d702ca 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -1951,7 +1951,7 @@ export class TeamProvisioningService { private toolApprovalEventEmitter: ((event: ToolApprovalEvent) => void) | null = null; private mainWindowRef: import('electron').BrowserWindow | null = null; - private activeApprovalNotifications = new Set(); + private activeApprovalNotifications = new Map(); setToolApprovalEventEmitter(emitter: (event: ToolApprovalEvent) => void): void { this.toolApprovalEventEmitter = emitter; @@ -5372,6 +5372,10 @@ export class TeamProvisioningService { const config = ConfigManager.getInstance().getConfig(); if (!config.notifications.enabled || !config.notifications.notifyOnToolApproval) return; + // Respect snooze — consistent with other notification types + const snoozedUntil = config.notifications.snoozedUntil; + if (snoozedUntil && Date.now() < snoozedUntil) return; + const { Notification: ElectronNotification } = require('electron') as typeof import('electron'); if (!ElectronNotification.isSupported()) return; @@ -5395,10 +5399,10 @@ export class TeamProvisioningService { : {}), }); - // Prevent GC from collecting the notification (macOS issue) - this.activeApprovalNotifications.add(notification); + // Track by requestId so we can close it when approval is resolved via UI + this.activeApprovalNotifications.set(approval.requestId, notification); const cleanup = (): void => { - this.activeApprovalNotifications.delete(notification); + this.activeApprovalNotifications.delete(approval.requestId); }; notification.on('click', () => { @@ -5436,6 +5440,23 @@ export class TeamProvisioningService { notification.show(); } + /** Dismiss the OS notification for a resolved/dismissed approval. */ + dismissApprovalNotification(requestId: string): void { + const notification = this.activeApprovalNotifications.get(requestId); + if (notification) { + notification.close(); + this.activeApprovalNotifications.delete(requestId); + } + } + + /** Dismiss all OS notifications for a team's approvals (e.g. on team stop). */ + private dismissAllApprovalNotifications(): void { + for (const [, notification] of this.activeApprovalNotifications) { + notification.close(); + } + this.activeApprovalNotifications.clear(); + } + private formatToolApprovalBody(toolName: string, toolInput: Record): string { switch (toolName) { case 'Bash': @@ -5511,6 +5532,7 @@ export class TeamProvisioningService { } run.pendingApprovals.delete(requestId); this.inFlightResponses.delete(requestId); + this.dismissApprovalNotification(requestId); this.emitToolApprovalEvent({ autoResolved: true, @@ -5683,6 +5705,7 @@ export class TeamProvisioningService { } finally { run.pendingApprovals.delete(requestId); this.inFlightResponses.delete(requestId); + this.dismissApprovalNotification(requestId); } } @@ -6244,6 +6267,7 @@ export class TeamProvisioningService { for (const requestId of run.pendingApprovals.keys()) { this.clearApprovalTimeout(requestId); this.inFlightResponses.delete(requestId); + this.dismissApprovalNotification(requestId); } this.emitToolApprovalEvent({ dismissed: true, teamName: run.teamName, runId: run.runId }); run.pendingApprovals.clear();