import React, { useState, useEffect, useReducer, useContext, createContext, useCallback } from 'react'; import { createRoot } from 'react-dom/client'; // Import shadcn Kanban components import { KanbanProvider, KanbanBoard, KanbanHeader, KanbanCards, KanbanCard, type DragEndEvent, type Status, type Feature } from '@/components/ui/shadcn-io/kanban'; // Import TaskDetailsView component import TaskDetailsView from '../components/TaskDetailsView'; // TypeScript interfaces for Task Master integration export interface TaskMasterTask { id: string; title: string; description: string; status: 'pending' | 'in-progress' | 'done' | 'deferred' | 'review'; priority: 'high' | 'medium' | 'low'; dependencies?: string[]; details?: string; testStrategy?: string; subtasks?: TaskMasterTask[]; complexityScore?: number; } interface WebviewMessage { type: string; requestId?: string; data?: any; success?: boolean; [key: string]: any; } // VS Code API declaration declare global { interface Window { acquireVsCodeApi?: () => { postMessage: (message: any) => void; setState: (state: any) => void; getState: () => any; }; } } // State management types interface AppState { tasks: TaskMasterTask[]; loading: boolean; error?: string; requestId: number; isConnected: boolean; connectionStatus: string; editingTask?: { taskId: string | null; editData?: TaskMasterTask }; polling: { isActive: boolean; errorCount: number; lastUpdate?: number; isUserInteracting: boolean; // Network status isOfflineMode: boolean; reconnectAttempts: number; maxReconnectAttempts: number; lastSuccessfulConnection?: number; connectionStatus: 'online' | 'offline' | 'reconnecting'; }; // Toast notifications toastNotifications: ToastNotification[]; // Navigation state currentView: 'kanban' | 'task-details'; selectedTaskId?: string; } // Add interface for task updates export interface TaskUpdates { title?: string; description?: string; details?: string; priority?: TaskMasterTask['priority']; testStrategy?: string; dependencies?: string[]; } // Add state for task editing type AppAction = | { type: 'SET_TASKS'; payload: TaskMasterTask[] } | { type: 'SET_LOADING'; payload: boolean } | { type: 'SET_ERROR'; payload: string } | { type: 'CLEAR_ERROR' } | { type: 'INCREMENT_REQUEST_ID' } | { type: 'UPDATE_TASK_STATUS'; payload: { taskId: string; newStatus: TaskMasterTask['status'] }; } | { type: 'UPDATE_TASK_CONTENT'; payload: { taskId: string; updates: TaskUpdates }; } | { type: 'SET_CONNECTION_STATUS'; payload: { isConnected: boolean; status: string }; } | { type: 'SET_EDITING_TASK'; payload: { taskId: string | null; editData?: TaskMasterTask }; } | { type: 'SET_POLLING_STATUS'; payload: { isActive: boolean; errorCount?: number }; } | { type: 'SET_USER_INTERACTING'; payload: boolean } | { type: 'TASKS_UPDATED_FROM_POLLING'; payload: TaskMasterTask[] } | { type: 'SET_NETWORK_STATUS'; payload: { isOfflineMode: boolean; connectionStatus: 'online' | 'offline' | 'reconnecting'; reconnectAttempts?: number; maxReconnectAttempts?: number; lastSuccessfulConnection?: number; }; } | { type: 'LOAD_CACHED_TASKS'; payload: TaskMasterTask[] } | { type: 'ADD_TOAST'; payload: ToastNotification } | { type: 'REMOVE_TOAST'; payload: string } | { type: 'CLEAR_ALL_TOASTS' } | { type: 'NAVIGATE_TO_TASK'; payload: string } | { type: 'NAVIGATE_TO_KANBAN' }; // Toast notification interfaces interface ToastNotification { id: string; type: 'success' | 'info' | 'warning' | 'error'; title: string; message: string; duration?: number; timestamp: number; } interface ErrorBoundaryState { hasError: boolean; error?: Error; errorInfo?: React.ErrorInfo; } // Error Boundary Component class ErrorBoundary extends React.Component< { children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void; }, ErrorBoundaryState > { constructor(props: { children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void; }) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('React Error Boundary caught an error:', error, errorInfo); this.setState({ error, errorInfo }); // Notify parent component of error if (this.props.onError) { this.props.onError(error, errorInfo); } // Send error to extension for centralized handling if (window.acquireVsCodeApi) { const vscode = window.acquireVsCodeApi(); vscode.postMessage({ type: 'reactError', data: { message: error.message, stack: error.stack, componentStack: errorInfo.componentStack, timestamp: Date.now() } }); } } render() { if (this.state.hasError) { return (

Something went wrong

The Task Master Kanban board encountered an unexpected error.

{this.state.error && (
Error Details
									{this.state.error.message}
									{this.state.error.stack && `\n\n${this.state.error.stack}`}
								
)}
); } return this.props.children; } } // Toast Notification Component const ToastNotification: React.FC<{ notification: ToastNotification; onDismiss: (id: string) => void; }> = ({ notification, onDismiss }) => { const [isVisible, setIsVisible] = useState(true); const [progress, setProgress] = useState(100); const duration = notification.duration || 5000; // 5 seconds default useEffect(() => { const progressInterval = setInterval(() => { setProgress((prev) => { const decrease = (100 / duration) * 100; // Update every 100ms return Math.max(0, prev - decrease); }); }, 100); const timeoutId = setTimeout(() => { setIsVisible(false); setTimeout(() => onDismiss(notification.id), 300); // Wait for animation }, duration); return () => { clearInterval(progressInterval); clearTimeout(timeoutId); }; }, [notification.id, duration, onDismiss]); const getIcon = () => { switch (notification.type) { case 'success': return ( ); case 'warning': return ( ); case 'error': return ( ); default: return ( ); } }; const getColorClasses = () => { switch (notification.type) { case 'success': return 'bg-green-500/10 border-green-500/30 text-green-400'; case 'warning': return 'bg-yellow-500/10 border-yellow-500/30 text-yellow-400'; case 'error': return 'bg-red-500/10 border-red-500/30 text-red-400'; default: return 'bg-blue-500/10 border-blue-500/30 text-blue-400'; } }; return (
{getIcon()}

{notification.title}

{notification.message}

{/* Progress bar */}
); }; // Toast Container Component const ToastContainer: React.FC<{ notifications: ToastNotification[]; onDismiss: (id: string) => void; }> = ({ notifications, onDismiss }) => { return (
{notifications.map((notification) => (
))}
); }; // Toast helper functions const createToast = ( type: ToastNotification['type'], title: string, message: string, duration?: number ): ToastNotification => { return { id: `toast_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type, title, message, duration, timestamp: Date.now() }; }; const showSuccessToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => { dispatch({ type: 'ADD_TOAST', payload: createToast('success', title, message, duration) }); }; const showInfoToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => { dispatch({ type: 'ADD_TOAST', payload: createToast('info', title, message, duration) }); }; const showWarningToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => { dispatch({ type: 'ADD_TOAST', payload: createToast('warning', title, message, duration) }); }; const showErrorToast = (dispatch: React.Dispatch) => (title: string, message: string, duration?: number) => { dispatch({ type: 'ADD_TOAST', payload: createToast('error', title, message, duration) }); }; // Kanban column configuration const kanbanStatuses: Status[] = [ { id: 'pending', name: 'To Do', color: '#6B7280' }, { id: 'in-progress', name: 'In Progress', color: '#F59E0B' }, { id: 'review', name: 'Review', color: '#8B5CF6' }, { id: 'done', name: 'Done', color: '#10B981' }, { id: 'deferred', name: 'Deferred', color: '#EF4444' } ]; // State reducer const appReducer = (state: AppState, action: AppAction): AppState => { switch (action.type) { case 'SET_TASKS': return { ...state, tasks: action.payload, loading: false, error: undefined }; case 'SET_LOADING': return { ...state, loading: action.payload }; case 'SET_ERROR': return { ...state, error: action.payload, loading: false }; case 'CLEAR_ERROR': return { ...state, error: undefined }; case 'INCREMENT_REQUEST_ID': return { ...state, requestId: state.requestId + 1 }; case 'UPDATE_TASK_STATUS': const updatedTasks = state.tasks.map((task) => task.id === action.payload.taskId ? { ...task, status: action.payload.newStatus } : task ); return { ...state, tasks: updatedTasks }; case 'UPDATE_TASK_CONTENT': const updatedTasksContent = state.tasks.map((task) => task.id === action.payload.taskId ? { ...task, ...action.payload.updates } : task ); return { ...state, tasks: updatedTasksContent }; case 'SET_CONNECTION_STATUS': return { ...state, isConnected: action.payload.isConnected, connectionStatus: action.payload.status }; case 'SET_EDITING_TASK': return { ...state, editingTask: action.payload }; case 'SET_POLLING_STATUS': return { ...state, polling: { ...state.polling, ...action.payload } }; case 'SET_USER_INTERACTING': return { ...state, polling: { ...state.polling, isUserInteracting: action.payload } }; case 'TASKS_UPDATED_FROM_POLLING': return { ...state, tasks: action.payload }; case 'SET_NETWORK_STATUS': return { ...state, polling: { ...state.polling, ...action.payload } }; case 'LOAD_CACHED_TASKS': return { ...state, tasks: action.payload }; case 'ADD_TOAST': return { ...state, toastNotifications: [...state.toastNotifications, action.payload] }; case 'REMOVE_TOAST': return { ...state, toastNotifications: state.toastNotifications.filter( (n) => n.id !== action.payload ) }; case 'CLEAR_ALL_TOASTS': return { ...state, toastNotifications: [] }; case 'NAVIGATE_TO_TASK': console.log('πŸ“ Reducer: Navigating to task', action.payload); return { ...state, currentView: 'task-details', selectedTaskId: action.payload }; case 'NAVIGATE_TO_KANBAN': console.log('πŸ“ Reducer: Navigating to kanban'); return { ...state, currentView: 'kanban', selectedTaskId: undefined }; default: return state; } }; // Context for VS Code API export const VSCodeContext = createContext<{ vscode?: ReturnType>; state: AppState; dispatch: React.Dispatch; sendMessage: (message: WebviewMessage) => Promise; availableHeight: number; // Toast notification functions showSuccessToast: (title: string, message: string, duration?: number) => void; showInfoToast: (title: string, message: string, duration?: number) => void; showWarningToast: (title: string, message: string, duration?: number) => void; showErrorToast: (title: string, message: string, duration?: number) => void; } | null>(null); // Priority Badge Component const PriorityBadge: React.FC<{ priority: TaskMasterTask['priority'] }> = ({ priority }) => { const colorMap = { high: 'bg-red-500/20 text-red-400 border-red-500/30', medium: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', low: 'bg-green-500/20 text-green-400 border-green-500/30' }; return ( {priority} ); }; // Task Edit Modal Component const TaskEditModal: React.FC<{ task: TaskMasterTask; onSave: (taskId: string, updates: TaskUpdates) => void; onCancel: () => void; }> = ({ task, onSave, onCancel }) => { const [formData, setFormData] = useState({ title: task.title, description: task.description, details: task.details || '', priority: task.priority, testStrategy: task.testStrategy || '', dependencies: task.dependencies || [] }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Only include changed fields const updates: TaskUpdates = {}; if (formData.title !== task.title) { updates.title = formData.title; } if (formData.description !== task.description) { updates.description = formData.description; } if (formData.details !== task.details) { updates.details = formData.details; } if (formData.priority !== task.priority) { updates.priority = formData.priority; } if (formData.testStrategy !== task.testStrategy) { updates.testStrategy = formData.testStrategy; } if ( JSON.stringify(formData.dependencies) !== JSON.stringify(task.dependencies) ) { updates.dependencies = formData.dependencies; } if (Object.keys(updates).length > 0) { onSave(task.id, updates); } else { onCancel(); // No changes made } }; const handleDependenciesChange = (value: string) => { const deps = value .split(',') .map((dep) => dep.trim()) .filter((dep) => dep.length > 0); setFormData((prev) => ({ ...prev, dependencies: deps })); }; return (

Edit Task #{task.id}

setFormData((prev) => ({ ...prev, title: e.target.value })) } className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder" required />