From c2fe77575718f4854700713c7add6de4b77bf1d3 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 14 Jul 2025 19:49:19 +0000 Subject: [PATCH] feat: add desktop notification system for user interactions - Add node-notifier dependency for cross-platform desktop notifications - Create NotificationService with support for Windows, macOS, and Linux - Add comprehensive VSCode settings for notification preferences - Integrate notifications with askApproval flow in presentAssistantMessage - Add error notifications to handleError function - Include localization support for all notification settings - Provide graceful fallback to VSCode notifications on failure Addresses #5015 --- pnpm-lock.yaml | 52 +++- .../presentAssistantMessage.ts | 54 ++++ src/package.json | 44 +++ src/package.nls.json | 10 +- .../notification/NotificationService.ts | 262 ++++++++++++++++++ src/services/notification/index.ts | 3 + src/services/notification/types.ts | 93 +++++++ 7 files changed, 516 insertions(+), 2 deletions(-) create mode 100644 src/services/notification/NotificationService.ts create mode 100644 src/services/notification/index.ts create mode 100644 src/services/notification/types.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5030055feac..4cb693b1ef3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -678,6 +678,9 @@ importers: node-ipc: specifier: ^12.0.0 version: 12.0.0 + node-notifier: + specifier: ^10.0.1 + version: 10.0.1 openai: specifier: ^5.0.0 version: 5.5.1(ws@8.18.2)(zod@3.25.61) @@ -808,6 +811,9 @@ importers: '@types/node-ipc': specifier: ^9.2.3 version: 9.2.3 + '@types/node-notifier': + specifier: ^8.0.5 + version: 8.0.5 '@types/proper-lockfile': specifier: ^4.1.4 version: 4.1.4 @@ -3841,6 +3847,9 @@ packages: '@types/node-ipc@9.2.3': resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==} + '@types/node-notifier@8.0.5': + resolution: {integrity: sha512-LX7+8MtTsv6szumAp6WOy87nqMEdGhhry/Qfprjm1Ma6REjVzeF7SCyvPtp5RaF6IkXCS9V4ra8g5fwvf2ZAYg==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -6000,6 +6009,9 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -6309,6 +6321,11 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6462,6 +6479,10 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -7447,6 +7468,9 @@ packages: resolution: {integrity: sha512-QHJ2gAJiqA3cM7cQiRjLsfCOBRB0TwQ6axYD4FSllQWipEbP6i7Se1dP8EzPKk5J1nCe27W69eqPmCoKyQ61Vg==} engines: {node: '>=14'} + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -8486,6 +8510,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + shiki@3.4.1: resolution: {integrity: sha512-PSnoczt+iWIOB4iRQ+XVPFtTuN1FcmuYzPgUBZTSv5pC6CozssIx2M4O5n4S9gJlUu9A3FxMU0ZPaHflky/6LA==} @@ -13043,6 +13070,10 @@ snapshots: dependencies: '@types/node': 20.17.57 + '@types/node-notifier@8.0.5': + dependencies: + '@types/node': 20.19.1 + '@types/node@12.20.55': {} '@types/node@14.18.63': {} @@ -13133,7 +13164,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.17.57 + '@types/node': 20.19.1 optional: true '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': @@ -15532,6 +15563,8 @@ snapshots: graphemer@1.4.0: {} + growly@1.3.0: {} + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -15892,6 +15925,8 @@ snapshots: is-decimal@2.0.1: {} + is-docker@2.2.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -16014,6 +16049,10 @@ snapshots: is-windows@1.0.2: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -17272,6 +17311,15 @@ snapshots: js-queue: 2.0.2 strong-type: 1.1.0 + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.7.2 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + node-releases@2.0.19: {} noms@0.0.0: @@ -18527,6 +18575,8 @@ snapshots: shell-quote@1.8.3: optional: true + shellwords@0.1.1: {} + shiki@3.4.1: dependencies: '@shikijs/core': 3.4.1 diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index ee3fa148b41..987e4f0fbf1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -6,6 +6,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import type { ToolParamName, ToolResponse } from "../../shared/tools" +import { NotificationService, NotificationType } from "../../services/notification" import { fetchInstructionsTool } from "../tools/fetchInstructionsTool" import { listFilesTool } from "../tools/listFilesTool" @@ -266,6 +267,47 @@ export async function presentAssistantMessage(cline: Task) { progressStatus?: ToolProgressStatus, isProtected?: boolean, ) => { + // Initialize notification service and send desktop notification for approval requests + const provider = cline.providerRef.deref() + if (provider) { + try { + const notificationService = new NotificationService(provider.context) + + // Determine the tool name from the current block for better notification context + let toolName = block.name + let notificationMessage = partialMessage || `Approval required for ${toolName}` + + // Customize notification message based on tool type + switch (toolName) { + case "execute_command": + notificationMessage = `Execute command: ${block.params.command}` + break + case "write_to_file": + notificationMessage = `Write to file: ${block.params.path}` + break + case "read_file": + // Handle both old and new read_file parameter formats + const filePath = + typeof block.params.args === "string" + ? "multiple files" + : block.params.path || "unknown" + notificationMessage = `Read file: ${filePath}` + break + case "browser_action": + notificationMessage = `Browser action: ${block.params.action}` + break + default: + notificationMessage = `${toolName} - Approval required` + break + } + + await notificationService.sendApprovalRequest(notificationMessage, toolName) + } catch (error) { + // Don't let notification errors break the approval flow + console.error("Failed to send desktop notification:", error) + } + } + const { response, text, images } = await cline.ask( type, partialMessage, @@ -307,6 +349,18 @@ export async function presentAssistantMessage(cline: Task) { const handleError = async (action: string, error: Error) => { const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` + // Send desktop notification for errors + const provider = cline.providerRef.deref() + if (provider) { + try { + const notificationService = new NotificationService(provider.context) + await notificationService.sendError(`Error ${action}`, error) + } catch (notificationError) { + // Don't let notification errors break the error handling flow + console.error("Failed to send error notification:", notificationError) + } + } + await cline.say( "error", `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`, diff --git a/src/package.json b/src/package.json index 9d87b6b01f1..42730249a1a 100644 --- a/src/package.json +++ b/src/package.json @@ -373,6 +373,48 @@ "type": "string", "default": "", "description": "%settings.autoImportSettingsPath.description%" + }, + "roo-cline.notifications.enabled": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.enabled.description%" + }, + "roo-cline.notifications.showApprovalRequests": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.showApprovalRequests.description%" + }, + "roo-cline.notifications.showErrors": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.showErrors.description%" + }, + "roo-cline.notifications.showTaskCompletion": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.showTaskCompletion.description%" + }, + "roo-cline.notifications.showUserInputRequired": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.showUserInputRequired.description%" + }, + "roo-cline.notifications.showSessionTimeouts": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.showSessionTimeouts.description%" + }, + "roo-cline.notifications.timeout": { + "type": "number", + "default": 10000, + "minimum": 0, + "maximum": 60000, + "description": "%settings.notifications.timeout.description%" + }, + "roo-cline.notifications.sound": { + "type": "boolean", + "default": true, + "description": "%settings.notifications.sound.description%" } } } @@ -432,6 +474,7 @@ "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", "node-ipc": "^12.0.0", + "node-notifier": "^10.0.1", "openai": "^5.0.0", "os-name": "^6.0.0", "p-limit": "^6.2.0", @@ -476,6 +519,7 @@ "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/node-cache": "^4.1.3", + "@types/node-notifier": "^8.0.5", "@types/node-ipc": "^9.2.3", "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", diff --git a/src/package.nls.json b/src/package.nls.json index c5225c45c8a..66bdee2ed10 100644 --- a/src/package.nls.json +++ b/src/package.nls.json @@ -34,5 +34,13 @@ "settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)", "settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')", "settings.enableCodeActions.description": "Enable Roo Code quick fixes", - "settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import." + "settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.", + "settings.notifications.enabled.description": "Enable desktop notifications for Roo Code events", + "settings.notifications.showApprovalRequests.description": "Show desktop notifications when approval is required for tool operations", + "settings.notifications.showErrors.description": "Show desktop notifications for errors and failures", + "settings.notifications.showTaskCompletion.description": "Show desktop notifications when tasks are completed", + "settings.notifications.showUserInputRequired.description": "Show desktop notifications when user input is required", + "settings.notifications.showSessionTimeouts.description": "Show desktop notifications for session timeouts", + "settings.notifications.timeout.description": "Notification timeout in milliseconds (0 = no timeout, max 60 seconds)", + "settings.notifications.sound.description": "Play sound with desktop notifications" } diff --git a/src/services/notification/NotificationService.ts b/src/services/notification/NotificationService.ts new file mode 100644 index 00000000000..712df5a7477 --- /dev/null +++ b/src/services/notification/NotificationService.ts @@ -0,0 +1,262 @@ +import * as vscode from "vscode" +import * as notifier from "node-notifier" +import * as path from "path" +import * as os from "os" +import { + NotificationConfig, + NotificationData, + NotificationType, + PlatformNotificationOptions, + DEFAULT_NOTIFICATION_CONFIG, +} from "./types" + +/** + * Cross-platform desktop notification service for Roo Code + * Provides OS-level notifications when the AI agent requires user interaction + */ +export class NotificationService { + private config: NotificationConfig + private readonly iconPath: string + + constructor(private readonly context: vscode.ExtensionContext) { + this.config = this.loadConfig() + this.iconPath = path.join(context.extensionPath, "assets", "icons", "icon.png") + } + + /** + * Load notification configuration from VSCode settings + */ + private loadConfig(): NotificationConfig { + const config = vscode.workspace.getConfiguration("roo-cline.notifications") + return { + enabled: config.get("enabled", DEFAULT_NOTIFICATION_CONFIG.enabled), + showApprovalRequests: config.get("showApprovalRequests", DEFAULT_NOTIFICATION_CONFIG.showApprovalRequests), + showErrors: config.get("showErrors", DEFAULT_NOTIFICATION_CONFIG.showErrors), + showTaskCompletion: config.get("showTaskCompletion", DEFAULT_NOTIFICATION_CONFIG.showTaskCompletion), + showUserInputRequired: config.get( + "showUserInputRequired", + DEFAULT_NOTIFICATION_CONFIG.showUserInputRequired, + ), + showSessionTimeouts: config.get("showSessionTimeouts", DEFAULT_NOTIFICATION_CONFIG.showSessionTimeouts), + timeout: config.get("timeout", DEFAULT_NOTIFICATION_CONFIG.timeout), + sound: config.get("sound", DEFAULT_NOTIFICATION_CONFIG.sound), + } + } + + /** + * Update configuration when settings change + */ + public updateConfig(): void { + this.config = this.loadConfig() + } + + /** + * Check if notifications should be shown for a specific type + */ + private shouldShowNotification(type: NotificationType): boolean { + if (!this.config.enabled) { + return false + } + + switch (type) { + case NotificationType.APPROVAL_REQUEST: + return this.config.showApprovalRequests + case NotificationType.ERROR: + return this.config.showErrors + case NotificationType.TASK_COMPLETION: + return this.config.showTaskCompletion + case NotificationType.USER_INPUT_REQUIRED: + return this.config.showUserInputRequired + case NotificationType.SESSION_TIMEOUT: + return this.config.showSessionTimeouts + default: + return false + } + } + + /** + * Get platform-specific notification options + */ + private getPlatformOptions(): PlatformNotificationOptions { + const platform = os.platform() + + switch (platform) { + case "win32": + return { + windows: { + appID: "RooCode.VSCodeExtension", + remove: this.config.timeout > 0, + }, + } + case "darwin": + return { + macos: { + bundleId: "com.roocode.vscode-extension", + critical: false, + }, + } + case "linux": + return { + linux: { + desktopEntry: "roo-code", + urgency: "normal", + }, + } + default: + return {} + } + } + + /** + * Send a desktop notification + */ + public async sendNotification(data: NotificationData): Promise { + if (!this.shouldShowNotification(data.type)) { + return + } + + try { + const platformOptions = this.getPlatformOptions() + const timeout = data.timeout ?? this.config.timeout + const sound = data.sound ?? this.config.sound + + // Prepare notification options based on platform + const notificationOptions: any = { + title: data.title, + message: data.message, + icon: data.icon || this.iconPath, + sound: sound, + wait: false, // Don't wait for user interaction + } + + // Add timeout if specified + if (timeout > 0) { + notificationOptions.timeout = timeout / 1000 // node-notifier expects seconds + } + + // Add platform-specific options + const platform = os.platform() + if (platform === "win32" && platformOptions.windows) { + notificationOptions.appID = platformOptions.windows.appID + notificationOptions.remove = platformOptions.windows.remove + } else if (platform === "darwin" && platformOptions.macos) { + notificationOptions.bundleId = platformOptions.macos.bundleId + notificationOptions.critical = platformOptions.macos.critical + } else if (platform === "linux" && platformOptions.linux) { + notificationOptions.hint = `string:desktop-entry:${platformOptions.linux.desktopEntry}` + notificationOptions.urgency = platformOptions.linux.urgency + } + + // Send the notification + await new Promise((resolve, reject) => { + notifier.notify(notificationOptions, (err: Error | null, response: any) => { + if (err) { + console.error("Failed to send notification:", err) + reject(err) + } else { + resolve() + } + }) + }) + } catch (error) { + console.error("Error sending desktop notification:", error) + // Fallback to VSCode notification if desktop notification fails + this.sendVSCodeFallbackNotification(data) + } + } + + /** + * Fallback to VSCode notification if desktop notification fails + */ + private sendVSCodeFallbackNotification(data: NotificationData): void { + const message = `${data.title}: ${data.message}` + + switch (data.type) { + case NotificationType.ERROR: + vscode.window.showErrorMessage(message) + break + case NotificationType.APPROVAL_REQUEST: + case NotificationType.USER_INPUT_REQUIRED: + vscode.window.showWarningMessage(message) + break + default: + vscode.window.showInformationMessage(message) + break + } + } + + /** + * Send an approval request notification + */ + public async sendApprovalRequest(message: string, toolName?: string): Promise { + const title = toolName ? `${toolName} - Approval Required` : "Approval Required" + await this.sendNotification({ + type: NotificationType.APPROVAL_REQUEST, + title, + message, + }) + } + + /** + * Send an error notification + */ + public async sendError(message: string, error?: Error): Promise { + const errorMessage = error ? `${message}: ${error.message}` : message + await this.sendNotification({ + type: NotificationType.ERROR, + title: "Roo Code Error", + message: errorMessage, + }) + } + + /** + * Send a task completion notification + */ + public async sendTaskCompletion(message: string): Promise { + await this.sendNotification({ + type: NotificationType.TASK_COMPLETION, + title: "Task Completed", + message, + }) + } + + /** + * Send a user input required notification + */ + public async sendUserInputRequired(message: string): Promise { + await this.sendNotification({ + type: NotificationType.USER_INPUT_REQUIRED, + title: "Input Required", + message, + }) + } + + /** + * Send a session timeout notification + */ + public async sendSessionTimeout(message: string): Promise { + await this.sendNotification({ + type: NotificationType.SESSION_TIMEOUT, + title: "Session Timeout", + message, + }) + } + + /** + * Test notification functionality + */ + public async testNotification(): Promise { + await this.sendNotification({ + type: NotificationType.APPROVAL_REQUEST, + title: "Roo Code Notification Test", + message: "Desktop notifications are working correctly!", + }) + } + + /** + * Dispose of the service + */ + public dispose(): void { + // Clean up any resources if needed + } +} diff --git a/src/services/notification/index.ts b/src/services/notification/index.ts new file mode 100644 index 00000000000..d053451ac3f --- /dev/null +++ b/src/services/notification/index.ts @@ -0,0 +1,3 @@ +export { NotificationService } from "./NotificationService" +export type { NotificationConfig, NotificationData, PlatformNotificationOptions } from "./types" +export { NotificationType, DEFAULT_NOTIFICATION_CONFIG } from "./types" diff --git a/src/services/notification/types.ts b/src/services/notification/types.ts new file mode 100644 index 00000000000..00663ca9dc9 --- /dev/null +++ b/src/services/notification/types.ts @@ -0,0 +1,93 @@ +/** + * Configuration interface for desktop notifications + */ +export interface NotificationConfig { + /** Whether desktop notifications are enabled */ + enabled: boolean + /** Whether to show notifications for approval requests */ + showApprovalRequests: boolean + /** Whether to show notifications for errors */ + showErrors: boolean + /** Whether to show notifications for task completion */ + showTaskCompletion: boolean + /** Whether to show notifications when user input is required */ + showUserInputRequired: boolean + /** Whether to show notifications for session timeouts */ + showSessionTimeouts: boolean + /** Timeout in milliseconds for notifications (0 = no timeout) */ + timeout: number + /** Whether to play sound with notifications */ + sound: boolean +} + +/** + * Default notification configuration + */ +export const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = { + enabled: true, + showApprovalRequests: true, + showErrors: true, + showTaskCompletion: true, + showUserInputRequired: true, + showSessionTimeouts: true, + timeout: 10000, // 10 seconds + sound: true, +} + +/** + * Types of notifications that can be sent + */ +export enum NotificationType { + APPROVAL_REQUEST = "approval_request", + ERROR = "error", + TASK_COMPLETION = "task_completion", + USER_INPUT_REQUIRED = "user_input_required", + SESSION_TIMEOUT = "session_timeout", +} + +/** + * Notification data structure + */ +export interface NotificationData { + /** Type of notification */ + type: NotificationType + /** Notification title */ + title: string + /** Notification message */ + message: string + /** Optional icon path */ + icon?: string + /** Whether to play sound */ + sound?: boolean + /** Timeout in milliseconds (0 = no timeout) */ + timeout?: number + /** Optional actions for the notification */ + actions?: string[] +} + +/** + * Platform-specific notification options + */ +export interface PlatformNotificationOptions { + /** Windows-specific options */ + windows?: { + /** App ID for Windows notifications */ + appID?: string + /** Whether to remove notification after timeout */ + remove?: boolean + } + /** macOS-specific options */ + macos?: { + /** Bundle ID for macOS notifications */ + bundleId?: string + /** Whether notification should be critical */ + critical?: boolean + } + /** Linux-specific options */ + linux?: { + /** Desktop entry name */ + desktopEntry?: string + /** Urgency level (low, normal, critical) */ + urgency?: "low" | "normal" | "critical" + } +}