Files
claude-task-master/apps/extension/src/utils/errorHandler.ts
DavidMaliglowka 64302dc191 feat(extension): complete VS Code extension with kanban board interface (#997)
---------
Co-authored-by: DavidMaliglowka <13022280+DavidMaliglowka@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-01 14:04:22 +02:00

859 lines
21 KiB
TypeScript

import * as vscode from 'vscode';
import { logger } from './logger';
import {
getNotificationType,
getToastDuration,
shouldShowNotification
} from './notificationPreferences';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
UI_RENDERING = 'ui_rendering',
VALIDATION = 'validation',
NETWORK = 'network',
INTERNAL = 'internal',
TASK_MASTER_API = 'TASK_MASTER_API',
DATA_VALIDATION = 'DATA_VALIDATION',
DATA_PARSING = 'DATA_PARSING',
TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
VSCODE_API = 'VSCODE_API',
WEBVIEW = 'WEBVIEW',
EXTENSION_HOST = 'EXTENSION_HOST',
USER_INTERACTION = 'USER_INTERACTION',
DRAG_DROP = 'DRAG_DROP',
COMPONENT_RENDER = 'COMPONENT_RENDER',
PERMISSION = 'PERMISSION',
FILE_SYSTEM = 'FILE_SYSTEM',
UNKNOWN = 'UNKNOWN'
}
export enum NotificationType {
VSCODE_INFO = 'VSCODE_INFO',
VSCODE_WARNING = 'VSCODE_WARNING',
VSCODE_ERROR = 'VSCODE_ERROR',
TOAST_SUCCESS = 'TOAST_SUCCESS',
TOAST_INFO = 'TOAST_INFO',
TOAST_WARNING = 'TOAST_WARNING',
TOAST_ERROR = 'TOAST_ERROR',
CONSOLE_ONLY = 'CONSOLE_ONLY',
SILENT = 'SILENT'
}
export interface ErrorContext {
// Core error information
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
// Contextual information
operation?: string; // What operation was being performed
taskId?: string; // Related task ID if applicable
userId?: string; // User context if applicable
sessionId?: string; // Session context
// Technical details
stackTrace?: string;
userAgent?: string;
timestamp?: number;
// Recovery information
isRecoverable?: boolean;
suggestedActions?: string[];
documentationLink?: string;
// Notification preferences
notificationType?: NotificationType;
showToUser?: boolean;
logToConsole?: boolean;
logToFile?: boolean;
}
export interface ErrorDetails {
code: string;
message: string;
category: ErrorCategory;
severity: ErrorSeverity;
timestamp: Date;
context?: Record<string, any>;
stack?: string;
userAction?: string;
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
}
export interface ErrorLogEntry {
id: string;
error: ErrorDetails;
resolved: boolean;
resolvedAt?: Date;
attempts: number;
lastAttempt?: Date;
}
/**
* Base class for all Task Master errors
*/
export abstract class TaskMasterError extends Error {
public readonly code: string;
public readonly category: ErrorCategory;
public readonly severity: ErrorSeverity;
public readonly timestamp: Date;
public readonly context?: Record<string, any>;
public readonly userAction?: string;
public readonly recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
constructor(
message: string,
code: string,
category: ErrorCategory,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
context?: Record<string, any>,
userAction?: string,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.category = category;
this.severity = severity;
this.timestamp = new Date();
this.context = context;
this.userAction = userAction;
this.recovery = recovery;
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
public toErrorDetails(): ErrorDetails {
return {
code: this.code,
message: this.message,
category: this.category,
severity: this.severity,
timestamp: this.timestamp,
context: this.context,
stack: this.stack,
userAction: this.userAction,
recovery: this.recovery
};
}
}
/**
* MCP Connection related errors
*/
export class MCPConnectionError extends TaskMasterError {
constructor(
message: string,
code = 'MCP_CONNECTION_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.MCP_CONNECTION,
ErrorSeverity.HIGH,
context,
'Check your Task Master configuration and ensure the MCP server is accessible.',
recovery
);
}
}
/**
* Configuration related errors
*/
export class ConfigurationError extends TaskMasterError {
constructor(
message: string,
code = 'CONFIGURATION_INVALID',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.CONFIGURATION,
ErrorSeverity.MEDIUM,
context,
'Check your Task Master configuration in VS Code settings.'
);
}
}
/**
* Task loading related errors
*/
export class TaskLoadingError extends TaskMasterError {
constructor(
message: string,
code = 'TASK_LOADING_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.TASK_LOADING,
ErrorSeverity.MEDIUM,
context,
'Try refreshing the task list or check your project configuration.',
recovery
);
}
}
/**
* UI rendering related errors
*/
export class UIRenderingError extends TaskMasterError {
constructor(
message: string,
code = 'UI_RENDERING_FAILED',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.UI_RENDERING,
ErrorSeverity.LOW,
context,
'Try closing and reopening the Kanban board.'
);
}
}
/**
* Network related errors
*/
export class NetworkError extends TaskMasterError {
constructor(
message: string,
code = 'NETWORK_ERROR',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.NETWORK,
ErrorSeverity.MEDIUM,
context,
'Check your network connection and firewall settings.',
recovery
);
}
}
/**
* Centralized error handler
*/
export class ErrorHandler {
private static instance: ErrorHandler | null = null;
private errorLog: ErrorLogEntry[] = [];
private maxLogSize = 1000;
private errorListeners: ((error: ErrorDetails) => void)[] = [];
private constructor() {
this.setupGlobalErrorHandlers();
}
static getInstance(): ErrorHandler {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
/**
* Handle an error with comprehensive logging and recovery
*/
async handleError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
const logEntry = this.logError(errorDetails);
// Notify listeners
this.notifyErrorListeners(errorDetails);
// Show user notification based on severity
await this.showUserNotification(errorDetails);
// Attempt recovery if available
if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
try {
await errorDetails.recovery.action();
this.markErrorResolved(logEntry.id);
} catch (recoveryError) {
logger.error('Error recovery failed:', recoveryError);
logEntry.attempts++;
logEntry.lastAttempt = new Date();
}
}
// Log to console with appropriate level
this.logToConsole(errorDetails);
}
/**
* Handle critical errors that should stop execution
*/
async handleCriticalError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
errorDetails.severity = ErrorSeverity.CRITICAL;
await this.handleError(error, context);
// Show critical error dialog
const action = await vscode.window.showErrorMessage(
`Critical Error in Task Master: ${errorDetails.message}`,
'View Details',
'Report Issue',
'Restart Extension'
);
switch (action) {
case 'View Details':
await this.showErrorDetails(errorDetails);
break;
case 'Report Issue':
await this.openIssueReport(errorDetails);
break;
case 'Restart Extension':
await vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
}
}
/**
* Add error event listener
*/
onError(listener: (error: ErrorDetails) => void): void {
this.errorListeners.push(listener);
}
/**
* Remove error event listener
*/
removeErrorListener(listener: (error: ErrorDetails) => void): void {
const index = this.errorListeners.indexOf(listener);
if (index !== -1) {
this.errorListeners.splice(index, 1);
}
}
/**
* Get error log
*/
getErrorLog(
category?: ErrorCategory,
severity?: ErrorSeverity
): ErrorLogEntry[] {
let filteredLog = this.errorLog;
if (category) {
filteredLog = filteredLog.filter(
(entry) => entry.error.category === category
);
}
if (severity) {
filteredLog = filteredLog.filter(
(entry) => entry.error.severity === severity
);
}
return filteredLog.slice().reverse(); // Most recent first
}
/**
* Clear error log
*/
clearErrorLog(): void {
this.errorLog = [];
}
/**
* Export error log for debugging
*/
exportErrorLog(): string {
return JSON.stringify(this.errorLog, null, 2);
}
/**
* Create error details from error instance
*/
private createErrorDetails(
error: Error | TaskMasterError,
context?: Record<string, any>
): ErrorDetails {
if (error instanceof TaskMasterError) {
const details = error.toErrorDetails();
if (context) {
details.context = { ...details.context, ...context };
}
return details;
}
// Handle standard Error objects
return {
code: 'UNKNOWN_ERROR',
message: error.message || 'An unknown error occurred',
category: ErrorCategory.INTERNAL,
severity: ErrorSeverity.MEDIUM,
timestamp: new Date(),
context: { ...context, errorName: error.name },
stack: error.stack
};
}
/**
* Log error to internal log
*/
private logError(errorDetails: ErrorDetails): ErrorLogEntry {
const logEntry: ErrorLogEntry = {
id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
error: errorDetails,
resolved: false,
attempts: 0
};
this.errorLog.push(logEntry);
// Maintain log size limit
if (this.errorLog.length > this.maxLogSize) {
this.errorLog = this.errorLog.slice(-this.maxLogSize);
}
return logEntry;
}
/**
* Mark error as resolved
*/
private markErrorResolved(errorId: string): void {
const entry = this.errorLog.find((e) => e.id === errorId);
if (entry) {
entry.resolved = true;
entry.resolvedAt = new Date();
}
}
/**
* Show user notification based on error severity and user preferences
*/
private async showUserNotification(
errorDetails: ErrorDetails
): Promise<void> {
// Check if user wants to see this notification
if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
return;
}
const notificationType = getNotificationType(
errorDetails.category,
errorDetails.severity
);
const message = errorDetails.userAction
? `${errorDetails.message} ${errorDetails.userAction}`
: errorDetails.message;
// Handle different notification types based on user preferences
switch (notificationType) {
case 'VSCODE_ERROR':
await vscode.window.showErrorMessage(message);
break;
case 'VSCODE_WARNING':
await vscode.window.showWarningMessage(message);
break;
case 'VSCODE_INFO':
await vscode.window.showInformationMessage(message);
break;
case 'TOAST_SUCCESS':
case 'TOAST_INFO':
case 'TOAST_WARNING':
case 'TOAST_ERROR':
// These will be handled by the webview toast system
// The error listener in extension.ts will send these to webview
break;
case 'CONSOLE_ONLY':
case 'SILENT':
// No user notification, just console logging
break;
default:
// Fallback to severity-based notifications
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.HIGH:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.MEDIUM:
await vscode.window.showWarningMessage(message);
break;
case ErrorSeverity.LOW:
await vscode.window.showInformationMessage(message);
break;
}
}
}
/**
* Log to console with appropriate level
*/
private logToConsole(errorDetails: ErrorDetails): void {
const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
logger.error(logMessage, errorDetails);
break;
case ErrorSeverity.MEDIUM:
logger.warn(logMessage, errorDetails);
break;
case ErrorSeverity.LOW:
console.info(logMessage, errorDetails);
break;
}
}
/**
* Show detailed error information
*/
private async showErrorDetails(errorDetails: ErrorDetails): Promise<void> {
const details = [
`Error Code: ${errorDetails.code}`,
`Category: ${errorDetails.category}`,
`Severity: ${errorDetails.severity}`,
`Time: ${errorDetails.timestamp.toISOString()}`,
`Message: ${errorDetails.message}`
];
if (errorDetails.context) {
details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
}
if (errorDetails.stack) {
details.push(`Stack Trace: ${errorDetails.stack}`);
}
const content = details.join('\n\n');
// Create temporary document to show error details
const doc = await vscode.workspace.openTextDocument({
content,
language: 'plaintext'
});
await vscode.window.showTextDocument(doc);
}
/**
* Open GitHub issue report
*/
private async openIssueReport(errorDetails: ErrorDetails): Promise<void> {
const issueTitle = encodeURIComponent(
`Error: ${errorDetails.code} - ${errorDetails.message}`
);
const issueBody = encodeURIComponent(`
**Error Details:**
- Code: ${errorDetails.code}
- Category: ${errorDetails.category}
- Severity: ${errorDetails.severity}
- Time: ${errorDetails.timestamp.toISOString()}
**Message:**
${errorDetails.message}
**Context:**
${errorDetails.context ? JSON.stringify(errorDetails.context, null, 2) : 'None'}
**Steps to Reproduce:**
1.
2.
3.
**Expected Behavior:**
**Additional Notes:**
`);
const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
}
/**
* Notify error listeners
*/
private notifyErrorListeners(errorDetails: ErrorDetails): void {
this.errorListeners.forEach((listener) => {
try {
listener(errorDetails);
} catch (error) {
logger.error('Error in error listener:', error);
}
});
}
/**
* Setup global error handlers
*/
private setupGlobalErrorHandlers(): void {
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const error = new InternalError(
'Unhandled Promise Rejection',
'UNHANDLED_REJECTION',
ErrorSeverity.HIGH,
{ reason: String(reason), promise: String(promise) }
);
this.handleError(error);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const taskMasterError = new InternalError(
'Uncaught Exception',
'UNCAUGHT_EXCEPTION',
ErrorSeverity.CRITICAL,
{ originalError: error.message, stack: error.stack }
);
this.handleCriticalError(taskMasterError);
});
}
}
/**
* Utility functions for error handling
*/
export function getErrorHandler(): ErrorHandler {
return ErrorHandler.getInstance();
}
export function createRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: false,
action,
description
};
}
export function createAutoRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: true,
action,
description
};
}
// Default error categorization rules
export const ERROR_CATEGORIZATION_RULES: Record<string, ErrorCategory> = {
// Network patterns
ECONNREFUSED: ErrorCategory.NETWORK,
ENOTFOUND: ErrorCategory.NETWORK,
ETIMEDOUT: ErrorCategory.NETWORK,
'Network request failed': ErrorCategory.NETWORK,
'fetch failed': ErrorCategory.NETWORK,
// MCP patterns
MCP: ErrorCategory.MCP_CONNECTION,
'Task Master': ErrorCategory.TASK_MASTER_API,
polling: ErrorCategory.TASK_MASTER_API,
// VS Code patterns
vscode: ErrorCategory.VSCODE_API,
webview: ErrorCategory.WEBVIEW,
extension: ErrorCategory.EXTENSION_HOST,
// Data patterns
JSON: ErrorCategory.DATA_PARSING,
parse: ErrorCategory.DATA_PARSING,
validation: ErrorCategory.DATA_VALIDATION,
invalid: ErrorCategory.DATA_VALIDATION,
// Permission patterns
EACCES: ErrorCategory.PERMISSION,
EPERM: ErrorCategory.PERMISSION,
permission: ErrorCategory.PERMISSION,
// File system patterns
ENOENT: ErrorCategory.FILE_SYSTEM,
EISDIR: ErrorCategory.FILE_SYSTEM,
file: ErrorCategory.FILE_SYSTEM
};
// Severity mapping based on error categories
export const CATEGORY_SEVERITY_MAPPING: Record<ErrorCategory, ErrorSeverity> = {
[ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
[ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
[ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
[ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
[ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
[ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
[ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
[ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
[ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
[ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
[ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
[ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
// Legacy mappings for existing categories
[ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
[ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
[ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
};
// Notification type mapping based on severity
export const SEVERITY_NOTIFICATION_MAPPING: Record<
ErrorSeverity,
NotificationType
> = {
[ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
[ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
[ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
[ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
};
/**
* Automatically categorize an error based on its message and type
*/
export function categorizeError(
error: Error | unknown,
operation?: string
): ErrorCategory {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const searchText =
`${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
for (const [pattern, category] of Object.entries(
ERROR_CATEGORIZATION_RULES
)) {
if (searchText.includes(pattern.toLowerCase())) {
return category;
}
}
return ErrorCategory.UNKNOWN;
}
export function getSuggestedSeverity(category: ErrorCategory): ErrorSeverity {
return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
}
export function getSuggestedNotificationType(
severity: ErrorSeverity
): NotificationType {
return (
SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY
);
}
export function createErrorContext(
error: Error | unknown,
operation?: string,
overrides?: Partial<ErrorContext>
): ErrorContext {
const category = categorizeError(error, operation);
const severity = getSuggestedSeverity(category);
const notificationType = getSuggestedNotificationType(severity);
const baseContext: ErrorContext = {
category,
severity,
message: error instanceof Error ? error.message : String(error),
originalError: error,
operation,
timestamp: Date.now(),
stackTrace: error instanceof Error ? error.stack : undefined,
isRecoverable: severity !== ErrorSeverity.CRITICAL,
notificationType,
showToUser:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
logToConsole: true,
logToFile:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
};
return { ...baseContext, ...overrides };
}