Files
claude-task-master/apps/extension/src/services/error-handler.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

331 lines
8.3 KiB
TypeScript

/**
* Error Handler Service
* Centralized error handling with categorization and recovery strategies
*/
import * as vscode from 'vscode';
import type { ExtensionLogger } from '../utils/logger';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
NETWORK = 'network',
INTERNAL = 'internal'
}
export interface ErrorContext {
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
operation?: string;
taskId?: string;
isRecoverable?: boolean;
suggestedActions?: string[];
}
export class ErrorHandler {
private errorLog: Map<string, ErrorContext> = new Map();
private errorId = 0;
constructor(private logger: ExtensionLogger) {}
/**
* Handle an error with appropriate logging and user notification
*/
handleError(context: ErrorContext): string {
const errorId = `error_${++this.errorId}`;
this.errorLog.set(errorId, context);
// Log to extension logger
this.logError(context);
// Show user notification if appropriate
this.notifyUser(context);
return errorId;
}
/**
* Log error based on severity
*/
private logError(context: ErrorContext): void {
const logMessage = `[${context.category}] ${context.message}`;
const details = {
operation: context.operation,
taskId: context.taskId,
error: context.originalError
};
switch (context.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
this.logger.error(logMessage, details);
break;
case ErrorSeverity.MEDIUM:
this.logger.warn(logMessage, details);
break;
case ErrorSeverity.LOW:
this.logger.debug(logMessage, details);
break;
}
}
/**
* Show user notification based on severity and category
*/
/**
* Validate if an action is allowed
*/
private isValidAction(action: string): boolean {
// Define predefined valid actions
const predefinedActions = [
'Retry',
'Settings',
'Reload',
'Dismiss',
'View Logs',
'Report Issue'
];
// Check if it's a predefined action or a TaskMaster command
return predefinedActions.includes(action) || action.startsWith('tm.');
}
/**
* Filter and validate suggested actions
*/
private getValidActions(actions: string[]): string[] {
return actions.filter((action) => this.isValidAction(action));
}
private notifyUser(context: ErrorContext): void {
// Don't show low severity errors to users
if (context.severity === ErrorSeverity.LOW) {
return;
}
// Validate and filter suggested actions
const rawActions = context.suggestedActions || [];
const actions = this.getValidActions(rawActions);
// Log if any actions were filtered out
if (rawActions.length !== actions.length) {
this.logger.warn('Invalid actions filtered out:', {
original: rawActions,
filtered: actions,
removed: rawActions.filter((a) => !actions.includes(a))
});
}
switch (context.severity) {
case ErrorSeverity.CRITICAL:
vscode.window
.showErrorMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
break;
case ErrorSeverity.HIGH:
if (context.category === ErrorCategory.MCP_CONNECTION) {
// Use validated actions or default actions for MCP connection
const mcpActions =
actions.length > 0 ? actions : ['Retry', 'Settings'];
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...mcpActions)
.then((action) => {
if (action === 'Retry') {
vscode.commands.executeCommand('tm.reconnect');
} else if (action === 'Settings') {
vscode.commands.executeCommand('tm.openSettings');
} else if (action) {
this.handleUserAction(action, context);
}
});
} else {
// Show warning with validated actions
if (actions.length > 0) {
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showWarningMessage(`TaskMaster: ${context.message}`);
}
}
break;
case ErrorSeverity.MEDIUM:
// Only show medium errors for important categories
if (
[ErrorCategory.CONFIGURATION, ErrorCategory.TASK_LOADING].includes(
context.category
)
) {
if (actions.length > 0) {
vscode.window
.showInformationMessage(
`TaskMaster: ${context.message}`,
...actions
)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showInformationMessage(
`TaskMaster: ${context.message}`
);
}
}
break;
}
}
/**
* Handle user action from notification
*/
private handleUserAction(action: string, context: ErrorContext): void {
this.logger.debug(`User selected action: ${action}`, {
errorContext: context
});
// Handle predefined actions
switch (action) {
case 'Retry':
if (context.category === ErrorCategory.MCP_CONNECTION) {
vscode.commands.executeCommand('tm.reconnect');
} else {
vscode.commands.executeCommand('tm.refreshTasks');
}
break;
case 'Settings':
vscode.commands.executeCommand('tm.openSettings');
break;
case 'Reload':
vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
case 'View Logs':
// Show error details in a modal dialog instead of output channel
this.showErrorDetails(context);
break;
case 'Report Issue':
const issueUrl = this.generateIssueUrl(context);
vscode.env.openExternal(vscode.Uri.parse(issueUrl));
break;
case 'Dismiss':
// No action needed
break;
default:
// Handle TaskMaster commands (tm.*)
if (action.startsWith('tm.')) {
void vscode.commands.executeCommand(action).then(
() => {},
(error: unknown) => {
this.logger.error(`Failed to execute command: ${action}`, error);
}
);
}
break;
}
}
/**
* Show detailed error information in a modal dialog
*/
private showErrorDetails(context: ErrorContext): void {
const details = [
`**Error Details**`,
``,
`Category: ${context.category}`,
`Severity: ${context.severity}`,
`Message: ${context.message}`,
context.operation ? `Operation: ${context.operation}` : '',
context.taskId ? `Task ID: ${context.taskId}` : '',
context.originalError ? `\nOriginal Error:\n${context.originalError}` : ''
]
.filter(Boolean)
.join('\n');
vscode.window.showInformationMessage(details, {
modal: true,
detail: details
});
}
/**
* Generate GitHub issue URL with pre-filled information
*/
private generateIssueUrl(context: ErrorContext): string {
const title = encodeURIComponent(`[Extension Error] ${context.message}`);
const body = encodeURIComponent(
[
`**Error Details:**`,
`- Category: ${context.category}`,
`- Severity: ${context.severity}`,
`- Message: ${context.message}`,
context.operation ? `- Operation: ${context.operation}` : '',
context.taskId ? `- Task ID: ${context.taskId}` : '',
``,
`**Context:**`,
'```json',
JSON.stringify(context, null, 2),
'```',
``,
`**Environment:**`,
`- VS Code Version: ${vscode.version}`,
`- Extension Version: ${vscode.extensions.getExtension('Hamster.taskmaster')?.packageJSON.version || 'Unknown'}`,
``,
`**Steps to Reproduce:**`,
`1. [Please describe the steps that led to this error]`,
``,
`**Expected Behavior:**`,
`[What should have happened instead]`
]
.filter(Boolean)
.join('\n')
);
return `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${title}&body=${body}`;
}
/**
* Get error by ID
*/
getError(errorId: string): ErrorContext | undefined {
return this.errorLog.get(errorId);
}
/**
* Clear old errors (keep last 100)
*/
clearOldErrors(): void {
if (this.errorLog.size > 100) {
const entriesToKeep = Array.from(this.errorLog.entries()).slice(-100);
this.errorLog.clear();
entriesToKeep.forEach(([id, error]) => this.errorLog.set(id, error));
}
}
}