/** * IPC Handlers for App Configuration. * * Handlers: * - config:get: Get full app configuration * - config:update: Update a specific config section * - config:addIgnoreRegex: Add an ignore pattern for notifications * - config:removeIgnoreRegex: Remove an ignore pattern * - config:addIgnoreRepository: Add a repository to ignore list * - config:removeIgnoreRepository: Remove a repository from ignore list * - config:snooze: Set snooze duration for notifications * - config:clearSnooze: Clear the snooze timer * - config:addTrigger: Add a new notification trigger * - config:updateTrigger: Update an existing notification trigger * - config:removeTrigger: Remove a notification trigger * - config:getTriggers: Get all notification triggers * - config:testTrigger: Test a trigger against historical session data */ import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';import { getErrorMessage } from '@shared/utils/errorHandling';import { createLogger } from '@shared/utils/logger';import { execFile } from 'child_process';import { BrowserWindow, dialog, type IpcMain, type IpcMainInvokeEvent } from 'electron';import * as fs from 'fs';import * as path from 'path';import { promisify } from 'util'; import { type AppConfig, ConfigManager, type NotificationTrigger, type TriggerContentType, type TriggerMatchField, type TriggerMode, type TriggerTokenType,} from '../services'; import { validateConfigUpdatePayload } from './configValidation';import { validateTriggerId } from './guards'; import type { TriggerColor } from '@shared/constants/triggerColors';import type { ClaudeRootFolderSelection, ClaudeRootInfo, WslClaudeRootCandidate,} from '@shared/types'; const logger = createLogger('IPC:config');const execFileAsync = promisify(execFile); // Get singleton instanceconst configManager = ConfigManager.getInstance();let onClaudeRootPathUpdated: ((claudeRootPath: string | null) => Promise<void> | void) | null = null; /** * Response type for config operations */interface ConfigResult<T = void> { success: boolean; data?: T; error?: string;} /** * Initializes config handlers with callbacks that require app-level services. */export function initializeConfigHandlers( options: { onClaudeRootPathUpdated?: (claudeRootPath: string | null) => Promise<void> | void; } = {}): void { onClaudeRootPathUpdated = options.onClaudeRootPathUpdated ?? null;} /** * Registers all config-related IPC handlers. */export function registerConfigHandlers(ipcMain: IpcMain): void { // Get full configuration ipcMain.handle('config:get', handleGetConfig); // Update configuration section ipcMain.handle('config:update', handleUpdateConfig); // Ignore regex pattern handlers ipcMain.handle('config:addIgnoreRegex', handleAddIgnoreRegex); ipcMain.handle('config:removeIgnoreRegex', handleRemoveIgnoreRegex); // Ignore repository handlers ipcMain.handle('config:addIgnoreRepository', handleAddIgnoreRepository); ipcMain.handle('config:removeIgnoreRepository', handleRemoveIgnoreRepository); // Snooze handlers ipcMain.handle('config:snooze', handleSnooze); ipcMain.handle('config:clearSnooze', handleClearSnooze); // Trigger management handlers ipcMain.handle('config:addTrigger', handleAddTrigger); ipcMain.handle('config:updateTrigger', handleUpdateTrigger); ipcMain.handle('config:removeTrigger', handleRemoveTrigger); ipcMain.handle('config:getTriggers', handleGetTriggers); ipcMain.handle('config:testTrigger', handleTestTrigger); // Session pin handlers ipcMain.handle('config:pinSession', handlePinSession); ipcMain.handle('config:unpinSession', handleUnpinSession); // Session hide handlers ipcMain.handle('config:hideSession', handleHideSession); ipcMain.handle('config:unhideSession', handleUnhideSession); ipcMain.handle('config:hideSessions', handleHideSessions); ipcMain.handle('config:unhideSessions', handleUnhideSessions); // Dialog handlers ipcMain.handle('config:selectFolders', handleSelectFolders); ipcMain.handle('config:selectClaudeRootFolder', handleSelectClaudeRootFolder); ipcMain.handle('config:getClaudeRootInfo', handleGetClaudeRootInfo); ipcMain.handle('config:findWslClaudeRoots', handleFindWslClaudeRoots); // Editor handlers ipcMain.handle('config:openInEditor', handleOpenInEditor); logger.info('Config handlers registered (including trigger management)');} // =============================================================================// Handler Functions// ============================================================================= /** * Handler for 'config:get' IPC call. * Returns the full app configuration. */async function handleGetConfig(_event: IpcMainInvokeEvent): Promise<ConfigResult<AppConfig>> { try { const config = configManager.getConfig(); return { success: true, data: config }; } catch (error) { logger.error('Error in config:get:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:update' IPC call. * Updates a specific section of the configuration. * Returns the full updated config. */async function handleUpdateConfig( _event: IpcMainInvokeEvent, section: unknown, data: unknown): Promise<ConfigResult<AppConfig>> { try { const validation = validateConfigUpdatePayload(section, data); if (!validation.valid) { return { success: false, error: validation.error }; } const isClaudeRootUpdate = validation.section === 'general' && Object.prototype.hasOwnProperty.call(validation.data, 'claudeRootPath'); configManager.updateConfig(validation.section, validation.data); if (isClaudeRootUpdate && onClaudeRootPathUpdated) { const nextClaudeRootPath = (validation.data as { claudeRootPath?: string | null }) .claudeRootPath; try { await onClaudeRootPathUpdated(nextClaudeRootPath ?? null); } catch (callbackError) { logger.error('Failed to apply updated Claude root path at runtime:', callbackError); } } const updatedConfig = configManager.getConfig(); return { success: true, data: updatedConfig }; } catch (error) { logger.error('Error in config:update:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:addIgnoreRegex' IPC call. * Adds a regex pattern to the notification ignore list. */async function handleAddIgnoreRegex( _event: IpcMainInvokeEvent, pattern: string): Promise<ConfigResult> { try { if (!pattern || typeof pattern !== 'string') { return { success: false, error: 'Pattern is required and must be a string' }; } // Validate that the pattern is a valid regex try { new RegExp(pattern); } catch { return { success: false, error: 'Invalid regex pattern' }; } configManager.addIgnoreRegex(pattern); return { success: true }; } catch (error) { logger.error('Error in config:addIgnoreRegex:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:removeIgnoreRegex' IPC call. * Removes a regex pattern from the notification ignore list. */async function handleRemoveIgnoreRegex( _event: IpcMainInvokeEvent, pattern: string): Promise<ConfigResult> { try { if (!pattern || typeof pattern !== 'string') { return { success: false, error: 'Pattern is required and must be a string' }; } configManager.removeIgnoreRegex(pattern); return { success: true }; } catch (error) { logger.error('Error in config:removeIgnoreRegex:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:addIgnoreRepository' IPC call. * Adds a repository to the notification ignore list. */async function handleAddIgnoreRepository( _event: IpcMainInvokeEvent, repositoryId: string): Promise<ConfigResult> { try { if (!repositoryId || typeof repositoryId !== 'string') { return { success: false, error: 'Repository ID is required and must be a string' }; } configManager.addIgnoreRepository(repositoryId); return { success: true }; } catch (error) { logger.error('Error in config:addIgnoreRepository:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:removeIgnoreRepository' IPC call. * Removes a repository from the notification ignore list. */async function handleRemoveIgnoreRepository( _event: IpcMainInvokeEvent, repositoryId: string): Promise<ConfigResult> { try { if (!repositoryId || typeof repositoryId !== 'string') { return { success: false, error: 'Repository ID is required and must be a string' }; } configManager.removeIgnoreRepository(repositoryId); return { success: true }; } catch (error) { logger.error('Error in config:removeIgnoreRepository:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:snooze' IPC call. * Sets the snooze timer for notifications. */async function handleSnooze(_event: IpcMainInvokeEvent, minutes: number): Promise<ConfigResult> { try { if (typeof minutes !== 'number' || minutes <= 0 || minutes > 24 * 60) { return { success: false, error: 'Minutes must be a positive number' }; } configManager.setSnooze(minutes); return { success: true }; } catch (error) { logger.error('Error in config:snooze:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:clearSnooze' IPC call. * Clears the snooze timer. */async function handleClearSnooze(_event: IpcMainInvokeEvent): Promise<ConfigResult> { try { configManager.clearSnooze(); return { success: true }; } catch (error) { logger.error('Error in config:clearSnooze:', error); return { success: false, error: getErrorMessage(error) }; }} /** * Handler for 'config:addTrigger' - Adds a new notification trigger. */async function handleAddTrigger( _event: IpcMainInvokeEvent, trigger: { id: string; name: string; enabled: boolean; contentType: string; mode?: TriggerMode; requireError?: boolean; toolName?: string; matchField?: string; matchPattern?: string; ignorePatterns?: string[]; tokenThreshold?: number; tokenType?: TriggerTokenType; repositoryIds?: string[]; color?: string; }): Promise<ConfigResult> { try { if (!trigger.id || !trigger.name || !trigger.contentType) { return { success: false, error: 'Trigger must have id, name, and contentType', }; } configManager.addTrigger({ id: trigger.id, name: trigger.name, enabled: trigger.enabled, contentType: trigger.contentType as TriggerContentType, mode: trigger.mode ?? (trigger.requireError ? 'error_status' : 'content_match'), requireError: trigger.requireError, toolName: trigger.toolName, matchField: trigger.matchField as TriggerMatchField | undefined, matchPattern: trigger.matchPattern, ignorePatterns: trigger.ignorePatterns, tokenThreshold: trigger.tokenThreshold, tokenType: trigger.tokenType, repositoryIds: trigger.repositoryIds, color: trigger.color as TriggerColor | undefined, isBuiltin: false, }); return { success: true }; } catch (error) { logger.error('Error in config:addTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to add trigger', }; }} /** * Handler for 'config:updateTrigger' - Updates an existing notification trigger. */async function handleUpdateTrigger( _event: IpcMainInvokeEvent, triggerId: string, updates: Partial<{ name: string; enabled: boolean; contentType: string; requireError: boolean; toolName: string; matchField: string; matchPattern: string; ignorePatterns: string[]; mode: TriggerMode; tokenThreshold: number; tokenType: TriggerTokenType; repositoryIds: string[]; color: string; }>): Promise<ConfigResult> { try { const validatedTriggerId = validateTriggerId(triggerId); if (!validatedTriggerId.valid) { return { success: false, error: validatedTriggerId.error ?? 'Trigger ID is required', }; } configManager.updateTrigger(validatedTriggerId.value!, updates as Partial<NotificationTrigger>); return { success: true }; } catch (error) { logger.error('Error in config:updateTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to update trigger', }; }} /** * Handler for 'config:removeTrigger' - Removes a notification trigger. */async function handleRemoveTrigger( _event: IpcMainInvokeEvent, triggerId: string): Promise<ConfigResult> { try { const validatedTriggerId = validateTriggerId(triggerId); if (!validatedTriggerId.valid) { return { success: false, error: validatedTriggerId.error ?? 'Trigger ID is required', }; } configManager.removeTrigger(validatedTriggerId.value!); return { success: true }; } catch (error) { logger.error('Error in config:removeTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to remove trigger', }; }} /** * Handler for 'config:getTriggers' - Gets all notification triggers. */async function handleGetTriggers( _event: IpcMainInvokeEvent): Promise<ConfigResult<NotificationTrigger[]>> { try { const triggers = configManager.getTriggers(); return { success: true, data: triggers }; } catch (error) { logger.error('Error in config:getTriggers:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to get triggers', }; }} /** * Handler for 'config:testTrigger' - Tests a trigger against historical session data. * Returns errors that would have been detected by the trigger. * * Safety: Results are truncated if: * - More than 10,000 total matches found * - More than 100 sessions scanned * - Test runs longer than 30 seconds */async function handleTestTrigger( _event: IpcMainInvokeEvent, trigger: NotificationTrigger): Promise< ConfigResult<{ totalCount: number; errors: { id: string; sessionId: string; projectId: string; message: string; timestamp: number; source: string; toolUseId?: string; subagentId?: string; lineNumber?: number; context: { projectName: string }; }[]; /** True if results were truncated due to safety limits */ truncated?: boolean; }>> { try { const { errorDetector } = await import('../services'); const result = await errorDetector.testTrigger(trigger, 50); // Map the DetectedError objects to the format expected by the renderer // Include toolUseId, subagentId, and lineNumber for deep linking to exact error location const errors = result.errors.map((error) => ({ id: error.id, sessionId: error.sessionId, projectId: error.projectId, message: error.message, timestamp: error.timestamp, source: error.source,Lines 1–500 of 1087. Load more
/** * IPC Handlers for App Configuration. * * Handlers: * - config:get: Get full app configuration * - config:update: Update a specific config section * - config:addIgnoreRegex: Add an ignore pattern for notifications * - config:removeIgnoreRegex: Remove an ignore pattern * - config:addIgnoreRepository: Add a repository to ignore list * - config:removeIgnoreRepository: Remove a repository from ignore list * - config:snooze: Set snooze duration for notifications * - config:clearSnooze: Clear the snooze timer * - config:addTrigger: Add a new notification trigger * - config:updateTrigger: Update an existing notification trigger * - config:removeTrigger: Remove a notification trigger * - config:getTriggers: Get all notification triggers * - config:testTrigger: Test a trigger against historical session data */ import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder'; import { getErrorMessage } from '@shared/utils/errorHandling'; import { createLogger } from '@shared/utils/logger'; import { execFile } from 'child_process'; import { BrowserWindow, dialog, type IpcMain, type IpcMainInvokeEvent } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import { promisify } from 'util'; import { type AppConfig, ConfigManager, type NotificationTrigger, type TriggerContentType, type TriggerMatchField, type TriggerMode, type TriggerTokenType, } from '../services'; import { validateConfigUpdatePayload } from './configValidation'; import { validateTriggerId } from './guards'; import type { TriggerColor } from '@shared/constants/triggerColors'; import type { ClaudeRootFolderSelection, ClaudeRootInfo, WslClaudeRootCandidate, } from '@shared/types'; const logger = createLogger('IPC:config'); const execFileAsync = promisify(execFile); // Get singleton instance const configManager = ConfigManager.getInstance(); let onClaudeRootPathUpdated: ((claudeRootPath: string | null) => Promise<void> | void) | null = null; /** * Response type for config operations */ interface ConfigResult<T = void> { success: boolean; data?: T; error?: string; } /** * Initializes config handlers with callbacks that require app-level services. */ export function initializeConfigHandlers( options: { onClaudeRootPathUpdated?: (claudeRootPath: string | null) => Promise<void> | void; } = {} ): void { onClaudeRootPathUpdated = options.onClaudeRootPathUpdated ?? null; } /** * Registers all config-related IPC handlers. */ export function registerConfigHandlers(ipcMain: IpcMain): void { // Get full configuration ipcMain.handle('config:get', handleGetConfig); // Update configuration section ipcMain.handle('config:update', handleUpdateConfig); // Ignore regex pattern handlers ipcMain.handle('config:addIgnoreRegex', handleAddIgnoreRegex); ipcMain.handle('config:removeIgnoreRegex', handleRemoveIgnoreRegex); // Ignore repository handlers ipcMain.handle('config:addIgnoreRepository', handleAddIgnoreRepository); ipcMain.handle('config:removeIgnoreRepository', handleRemoveIgnoreRepository); // Snooze handlers ipcMain.handle('config:snooze', handleSnooze); ipcMain.handle('config:clearSnooze', handleClearSnooze); // Trigger management handlers ipcMain.handle('config:addTrigger', handleAddTrigger); ipcMain.handle('config:updateTrigger', handleUpdateTrigger); ipcMain.handle('config:removeTrigger', handleRemoveTrigger); ipcMain.handle('config:getTriggers', handleGetTriggers); ipcMain.handle('config:testTrigger', handleTestTrigger); // Session pin handlers ipcMain.handle('config:pinSession', handlePinSession); ipcMain.handle('config:unpinSession', handleUnpinSession); // Session hide handlers ipcMain.handle('config:hideSession', handleHideSession); ipcMain.handle('config:unhideSession', handleUnhideSession); ipcMain.handle('config:hideSessions', handleHideSessions); ipcMain.handle('config:unhideSessions', handleUnhideSessions); // Dialog handlers ipcMain.handle('config:selectFolders', handleSelectFolders); ipcMain.handle('config:selectClaudeRootFolder', handleSelectClaudeRootFolder); ipcMain.handle('config:getClaudeRootInfo', handleGetClaudeRootInfo); ipcMain.handle('config:findWslClaudeRoots', handleFindWslClaudeRoots); // Editor handlers ipcMain.handle('config:openInEditor', handleOpenInEditor); logger.info('Config handlers registered (including trigger management)'); } // ============================================================================= // Handler Functions // ============================================================================= /** * Handler for 'config:get' IPC call. * Returns the full app configuration. */ async function handleGetConfig(_event: IpcMainInvokeEvent): Promise<ConfigResult<AppConfig>> { try { const config = configManager.getConfig(); return { success: true, data: config }; } catch (error) { logger.error('Error in config:get:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:update' IPC call. * Updates a specific section of the configuration. * Returns the full updated config. */ async function handleUpdateConfig( _event: IpcMainInvokeEvent, section: unknown, data: unknown ): Promise<ConfigResult<AppConfig>> { try { const validation = validateConfigUpdatePayload(section, data); if (!validation.valid) { return { success: false, error: validation.error }; } const isClaudeRootUpdate = validation.section === 'general' && Object.prototype.hasOwnProperty.call(validation.data, 'claudeRootPath'); configManager.updateConfig(validation.section, validation.data); if (isClaudeRootUpdate && onClaudeRootPathUpdated) { const nextClaudeRootPath = (validation.data as { claudeRootPath?: string | null }) .claudeRootPath; try { await onClaudeRootPathUpdated(nextClaudeRootPath ?? null); } catch (callbackError) { logger.error('Failed to apply updated Claude root path at runtime:', callbackError); } } const updatedConfig = configManager.getConfig(); return { success: true, data: updatedConfig }; } catch (error) { logger.error('Error in config:update:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:addIgnoreRegex' IPC call. * Adds a regex pattern to the notification ignore list. */ async function handleAddIgnoreRegex( _event: IpcMainInvokeEvent, pattern: string ): Promise<ConfigResult> { try { if (!pattern || typeof pattern !== 'string') { return { success: false, error: 'Pattern is required and must be a string' }; } // Validate that the pattern is a valid regex try { new RegExp(pattern); } catch { return { success: false, error: 'Invalid regex pattern' }; } configManager.addIgnoreRegex(pattern); return { success: true }; } catch (error) { logger.error('Error in config:addIgnoreRegex:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:removeIgnoreRegex' IPC call. * Removes a regex pattern from the notification ignore list. */ async function handleRemoveIgnoreRegex( _event: IpcMainInvokeEvent, pattern: string ): Promise<ConfigResult> { try { if (!pattern || typeof pattern !== 'string') { return { success: false, error: 'Pattern is required and must be a string' }; } configManager.removeIgnoreRegex(pattern); return { success: true }; } catch (error) { logger.error('Error in config:removeIgnoreRegex:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:addIgnoreRepository' IPC call. * Adds a repository to the notification ignore list. */ async function handleAddIgnoreRepository( _event: IpcMainInvokeEvent, repositoryId: string ): Promise<ConfigResult> { try { if (!repositoryId || typeof repositoryId !== 'string') { return { success: false, error: 'Repository ID is required and must be a string' }; } configManager.addIgnoreRepository(repositoryId); return { success: true }; } catch (error) { logger.error('Error in config:addIgnoreRepository:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:removeIgnoreRepository' IPC call. * Removes a repository from the notification ignore list. */ async function handleRemoveIgnoreRepository( _event: IpcMainInvokeEvent, repositoryId: string ): Promise<ConfigResult> { try { if (!repositoryId || typeof repositoryId !== 'string') { return { success: false, error: 'Repository ID is required and must be a string' }; } configManager.removeIgnoreRepository(repositoryId); return { success: true }; } catch (error) { logger.error('Error in config:removeIgnoreRepository:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:snooze' IPC call. * Sets the snooze timer for notifications. */ async function handleSnooze(_event: IpcMainInvokeEvent, minutes: number): Promise<ConfigResult> { try { if (typeof minutes !== 'number' || minutes <= 0 || minutes > 24 * 60) { return { success: false, error: 'Minutes must be a positive number' }; } configManager.setSnooze(minutes); return { success: true }; } catch (error) { logger.error('Error in config:snooze:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:clearSnooze' IPC call. * Clears the snooze timer. */ async function handleClearSnooze(_event: IpcMainInvokeEvent): Promise<ConfigResult> { try { configManager.clearSnooze(); return { success: true }; } catch (error) { logger.error('Error in config:clearSnooze:', error); return { success: false, error: getErrorMessage(error) }; } } /** * Handler for 'config:addTrigger' - Adds a new notification trigger. */ async function handleAddTrigger( _event: IpcMainInvokeEvent, trigger: { id: string; name: string; enabled: boolean; contentType: string; mode?: TriggerMode; requireError?: boolean; toolName?: string; matchField?: string; matchPattern?: string; ignorePatterns?: string[]; tokenThreshold?: number; tokenType?: TriggerTokenType; repositoryIds?: string[]; color?: string; } ): Promise<ConfigResult> { try { if (!trigger.id || !trigger.name || !trigger.contentType) { return { success: false, error: 'Trigger must have id, name, and contentType', }; } configManager.addTrigger({ id: trigger.id, name: trigger.name, enabled: trigger.enabled, contentType: trigger.contentType as TriggerContentType, mode: trigger.mode ?? (trigger.requireError ? 'error_status' : 'content_match'), requireError: trigger.requireError, toolName: trigger.toolName, matchField: trigger.matchField as TriggerMatchField | undefined, matchPattern: trigger.matchPattern, ignorePatterns: trigger.ignorePatterns, tokenThreshold: trigger.tokenThreshold, tokenType: trigger.tokenType, repositoryIds: trigger.repositoryIds, color: trigger.color as TriggerColor | undefined, isBuiltin: false, }); return { success: true }; } catch (error) { logger.error('Error in config:addTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to add trigger', }; } } /** * Handler for 'config:updateTrigger' - Updates an existing notification trigger. */ async function handleUpdateTrigger( _event: IpcMainInvokeEvent, triggerId: string, updates: Partial<{ name: string; enabled: boolean; contentType: string; requireError: boolean; toolName: string; matchField: string; matchPattern: string; ignorePatterns: string[]; mode: TriggerMode; tokenThreshold: number; tokenType: TriggerTokenType; repositoryIds: string[]; color: string; }> ): Promise<ConfigResult> { try { const validatedTriggerId = validateTriggerId(triggerId); if (!validatedTriggerId.valid) { return { success: false, error: validatedTriggerId.error ?? 'Trigger ID is required', }; } configManager.updateTrigger(validatedTriggerId.value!, updates as Partial<NotificationTrigger>); return { success: true }; } catch (error) { logger.error('Error in config:updateTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to update trigger', }; } } /** * Handler for 'config:removeTrigger' - Removes a notification trigger. */ async function handleRemoveTrigger( _event: IpcMainInvokeEvent, triggerId: string ): Promise<ConfigResult> { try { const validatedTriggerId = validateTriggerId(triggerId); if (!validatedTriggerId.valid) { return { success: false, error: validatedTriggerId.error ?? 'Trigger ID is required', }; } configManager.removeTrigger(validatedTriggerId.value!); return { success: true }; } catch (error) { logger.error('Error in config:removeTrigger:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to remove trigger', }; } } /** * Handler for 'config:getTriggers' - Gets all notification triggers. */ async function handleGetTriggers( _event: IpcMainInvokeEvent ): Promise<ConfigResult<NotificationTrigger[]>> { try { const triggers = configManager.getTriggers(); return { success: true, data: triggers }; } catch (error) { logger.error('Error in config:getTriggers:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to get triggers', }; } } /** * Handler for 'config:testTrigger' - Tests a trigger against historical session data. * Returns errors that would have been detected by the trigger. * * Safety: Results are truncated if: * - More than 10,000 total matches found * - More than 100 sessions scanned * - Test runs longer than 30 seconds */ async function handleTestTrigger( _event: IpcMainInvokeEvent, trigger: NotificationTrigger ): Promise< ConfigResult<{ totalCount: number; errors: { id: string; sessionId: string; projectId: string; message: string; timestamp: number; source: string; toolUseId?: string; subagentId?: string; lineNumber?: number; context: { projectName: string }; }[]; /** True if results were truncated due to safety limits */ truncated?: boolean; }> > { try { const { errorDetector } = await import('../services'); const result = await errorDetector.testTrigger(trigger, 50); // Map the DetectedError objects to the format expected by the renderer // Include toolUseId, subagentId, and lineNumber for deep linking to exact error location const errors = result.errors.map((error) => ({ id: error.id, sessionId: error.sessionId, projectId: error.projectId, message: error.message, timestamp: error.timestamp, source: error.source,