// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { injectable, inject } from '@theia/core/shared/inversify'; import { MessageService } from '@theia/core/lib/common/message-service'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { nls } from '@theia/core/lib/common/nls'; import { PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE, } from '../common/ai-core-preferences'; import { AgentService } from '../common/agent-service'; import { AISettingsService } from '../common/settings-service'; import { OSNotificationService } from './os-notification-service'; import { WindowBlinkService } from './window-blink-service'; import { NotificationType, NOTIFICATION_TYPE_OFF, NOTIFICATION_TYPE_OS_NOTIFICATION, NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_TYPE_BLINK, } from '../common/notification-types'; import { PreferenceService } from '@theia/core'; @injectable() export class AgentCompletionNotificationService { @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @inject(AgentService) protected readonly agentService: AgentService; @inject(AISettingsService) protected readonly settingsService: AISettingsService; @inject(OSNotificationService) protected readonly osNotificationService: OSNotificationService; @inject(MessageService) protected readonly messageService: MessageService; @inject(WindowBlinkService) protected readonly windowBlinkService: WindowBlinkService; @inject(ApplicationShell) protected readonly shell: ApplicationShell; /** * Show a completion notification for the specified agent if enabled in preferences. * * @param agentId The unique identifier of the agent * @param taskDescription Optional description of the completed task */ async showCompletionNotification( agentId: string, taskDescription?: string, ): Promise { const notificationType = await this.getNotificationTypeForAgent(agentId); if (notificationType === NOTIFICATION_TYPE_OFF || this.isChatWidgetFocused()) { return; } try { const agentName = this.resolveAgentName(agentId); await this.executeNotificationType( agentName, taskDescription, notificationType, ); } catch (error) { console.error( 'Failed to show agent completion notification:', error, ); } } /** * Resolve the display name for an agent by its ID. * * @param agentId The unique identifier of the agent * @returns The agent's display name or the agent ID if not found */ protected resolveAgentName(agentId: string): string { try { const agents = this.agentService.getAllAgents(); const agent = agents.find(a => a.id === agentId); return agent?.name || agentId; } catch (error) { console.warn( `Failed to resolve agent name for ID '${agentId}':`, error, ); return agentId; } } /** * Get the preferred notification type for a specific agent. * If no agent-specific preference is set, returns the global default notification type. */ protected async getNotificationTypeForAgent( agentId: string, ): Promise { const agentSettings = await this.settingsService.getAgentSettings(agentId); const agentNotificationType = agentSettings?.completionNotification as NotificationType; // If agent has no specific setting, use the global default if (!agentNotificationType) { return this.preferenceService.get( PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE, NOTIFICATION_TYPE_OFF, ); } return agentNotificationType; } /** * Execute the specified notification type. */ private async executeNotificationType( agentName: string, taskDescription: string | undefined, type: NotificationType, ): Promise { switch (type) { case NOTIFICATION_TYPE_OS_NOTIFICATION: await this.showOSNotification(agentName, taskDescription); break; case NOTIFICATION_TYPE_MESSAGE: await this.showMessageServiceNotification( agentName, taskDescription, ); break; case NOTIFICATION_TYPE_BLINK: await this.showBlinkNotification(agentName); break; default: throw new Error(`Unknown notification type: ${type}`); } } /** * Show OS notification directly. */ protected async showOSNotification( agentName: string, taskDescription?: string, ): Promise { const result = await this.osNotificationService.showAgentCompletionNotification( agentName, taskDescription, ); if (!result.success) { throw new Error(`OS notification failed: ${result.error}`); } } /** * Show MessageService notification. */ protected async showMessageServiceNotification( agentName: string, taskDescription?: string, ): Promise { const message = taskDescription ? nls.localize( 'theia/ai-core/agentCompletionWithTask', 'Agent "{0}" has completed the task: {1}', agentName, taskDescription, ) : nls.localize( 'theia/ai-core/agentCompletionMessage', 'Agent "{0}" has completed its task.', agentName, ); this.messageService.info(message); } /** * Show window blink notification. */ protected async showBlinkNotification(agentName: string): Promise { const result = await this.windowBlinkService.blinkWindow(agentName); if (!result.success) { throw new Error( `Window blink notification failed: ${result.error}`, ); } } /** * Check if OS notifications are supported and enabled. */ isOSNotificationSupported(): boolean { return this.osNotificationService.isNotificationSupported(); } /** * Get the current OS notification permission status. */ getOSNotificationPermission(): NotificationPermission { return this.osNotificationService.getPermissionStatus(); } /** * Request OS notification permission from the user. */ async requestOSNotificationPermission(): Promise { return this.osNotificationService.requestPermission(); } /** * Check if any chat widget currently has focus. */ protected isChatWidgetFocused(): boolean { const activeWidget = this.shell.activeWidget; if (!activeWidget) { return false; } return activeWidget.id === 'chat-view-widget'; } }