fix: OS notification lifecycle — dismiss on resolve, respect snooze

- 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
This commit is contained in:
iliya 2026-03-21 13:34:03 +02:00
parent 4d65bad24f
commit 0d74619241

View file

@ -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<import('electron').Notification>();
private activeApprovalNotifications = new Map<string, import('electron').Notification>();
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, unknown>): 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();