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 (
);
};
// Task Card Component
const TaskCard: React.FC<{
task: TaskMasterTask;
index: number;
status: string;
onEdit?: (task: TaskMasterTask) => void;
onViewDetails?: (taskId: string) => void;
}> = ({ task, index, status, onEdit, onViewDetails }) => {
const handleClick = (e: React.MouseEvent) => {
onViewDetails?.(task.id);
};
const handleDoubleClick = (e: React.MouseEvent) => {
onViewDetails?.(task.id);
};
return (
{task.description && (
{task.description}
)}
#{task.id}
{task.dependencies && task.dependencies.length > 0 && (
Deps: {task.dependencies.length}
)}
);
};
// Main Kanban Board Component
const TaskMasterKanban: React.FC = () => {
const context = useContext(VSCodeContext);
if (!context) {
throw new Error('TaskMasterKanban must be used within VSCodeContext');
}
const { state, dispatch, sendMessage, availableHeight } = context;
const {
tasks,
loading,
error,
editingTask,
polling,
currentView,
selectedTaskId
} = state;
const [activeTask, setActiveTask] = React.useState(
null
);
// Calculate header height for proper kanban board sizing
const headerHeight = 73; // Header with padding and border
const kanbanHeight = availableHeight - headerHeight;
// Group tasks by status
const tasksByStatus = kanbanStatuses.reduce(
(acc, status) => {
acc[status.id] = tasks.filter((task) => task.status === status.id);
return acc;
},
{} as Record
);
// Handle task update
const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
console.log(`π Updating task ${taskId} content:`, updates);
// Optimistic update
dispatch({
type: 'UPDATE_TASK_CONTENT',
payload: { taskId, updates }
});
try {
// Send update to extension
await sendMessage({
type: 'updateTask',
data: {
taskId,
updates,
options: { append: false, research: false }
}
});
console.log(`β
Task ${taskId} content updated successfully`);
// Close the edit modal
dispatch({
type: 'SET_EDITING_TASK',
payload: { taskId: null }
});
} catch (error) {
console.error(`β Failed to update task ${taskId}:`, error);
// Revert the optimistic update on error
const originalTask = editingTask?.editData;
if (originalTask) {
dispatch({
type: 'UPDATE_TASK_CONTENT',
payload: {
taskId,
updates: {
title: originalTask.title,
description: originalTask.description,
details: originalTask.details,
priority: originalTask.priority,
testStrategy: originalTask.testStrategy,
dependencies: originalTask.dependencies
}
}
});
}
dispatch({
type: 'SET_ERROR',
payload: `Failed to update task: ${error}`
});
}
};
// Handle drag start - mark user as interacting and set active task
const handleDragStart = (event: DragEndEvent) => {
console.log('π±οΈ User started dragging, pausing updates');
dispatch({ type: 'SET_USER_INTERACTING', payload: true });
const taskId = event.active.id as string;
const task = tasks.find((t) => t.id === taskId);
setActiveTask(task || null);
};
// Handle drag end - allow updates again after a delay
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
// Clear active task
setActiveTask(null);
// Re-enable updates after drag completes
setTimeout(() => {
console.log('β
Drag completed, resuming updates');
dispatch({ type: 'SET_USER_INTERACTING', payload: false });
}, 1000); // 1 second delay to ensure smooth completion
if (!over) {
return;
}
const taskId = active.id as string;
const newStatus = over.id as TaskMasterTask['status'];
// Find the task that was moved
const task = tasks.find((t) => t.id === taskId);
if (!task || task.status === newStatus) {
return;
}
console.log(`π Moving task ${taskId} from ${task.status} to ${newStatus}`);
// Update task status locally (optimistic update)
dispatch({
type: 'UPDATE_TASK_STATUS',
payload: { taskId, newStatus }
});
try {
// Send update to extension
await sendMessage({
type: 'updateTaskStatus',
data: { taskId, newStatus, oldStatus: task.status }
});
console.log(`β
Task ${taskId} status updated successfully`);
} catch (error) {
console.error(`β Failed to update task ${taskId}:`, error);
// Revert the optimistic update on error
dispatch({
type: 'UPDATE_TASK_STATUS',
payload: { taskId, newStatus: task.status }
});
dispatch({
type: 'SET_ERROR',
payload: `Failed to update task status: ${error}`
});
}
};
// Get polling status indicator
const getPollingStatusIndicator = () => {
const {
isActive,
errorCount,
isOfflineMode,
connectionStatus,
reconnectAttempts,
maxReconnectAttempts
} = polling;
if (isOfflineMode || connectionStatus === 'offline') {
return (
);
} else if (connectionStatus === 'reconnecting') {
return (
);
} else if (isActive) {
return (
);
} else if (errorCount > 0) {
return (
);
} else {
return (
);
}
};
if (loading) {
return (
);
}
if (error) {
return (
Error: {error}
);
}
return (
<>
Task Master Kanban
{getPollingStatusIndicator()}
{}}
onViewDetails={() => {}}
/>
) : null
}
>
{kanbanStatuses.map((status) => {
const columnHeaderHeight = 49; // Header with padding and border
const columnPadding = 16; // p-2 = 8px top + 8px bottom
const availableColumnHeight =
kanbanHeight - columnHeaderHeight - columnPadding;
return (
{tasksByStatus[status.id]?.map((task, index) => (
{
dispatch({
type: 'SET_EDITING_TASK',
payload: { taskId: task.id, editData: task }
});
}}
onViewDetails={(taskId) => {
console.log(
'π Navigating to task details:',
taskId
);
dispatch({
type: 'NAVIGATE_TO_TASK',
payload: taskId
});
}}
/>
))}
);
})}
{/* Task Edit Modal */}
{editingTask?.taskId && editingTask.editData && (
{
await handleUpdateTask(taskId, updates);
}}
onCancel={() => {
dispatch({
type: 'SET_EDITING_TASK',
payload: { taskId: null }
});
}}
/>
)}
>
);
};
// Main App Component
const App: React.FC = () => {
const [state, dispatch] = useReducer(appReducer, {
tasks: [],
loading: true,
requestId: 0,
isConnected: false,
connectionStatus: 'Connecting...',
editingTask: { taskId: null },
polling: {
isActive: false,
errorCount: 0,
lastUpdate: undefined,
isUserInteracting: false,
// Network status
isOfflineMode: false,
reconnectAttempts: 0,
maxReconnectAttempts: 0,
lastSuccessfulConnection: undefined,
connectionStatus: 'online'
},
toastNotifications: [],
currentView: 'kanban',
selectedTaskId: undefined
});
const [vscode] = useState(() => {
return window.acquireVsCodeApi?.();
});
const [pendingRequests] = useState(
new Map<
string,
{ resolve: Function; reject: Function; timeout: NodeJS.Timeout }
>()
);
// Dynamic height calculation state
const [availableHeight, setAvailableHeight] = useState(
window.innerHeight
);
// Calculate available height for kanban board
const updateAvailableHeight = useCallback(() => {
// Use window.innerHeight to get the actual available space
// This automatically accounts for VS Code panels like terminal, problems, etc.
const height = window.innerHeight;
console.log('π Available height updated:', height);
setAvailableHeight(height);
}, []);
// Listen to resize events to handle VS Code panel changes
useEffect(() => {
updateAvailableHeight();
const handleResize = () => {
updateAvailableHeight();
};
window.addEventListener('resize', handleResize);
// Also listen for VS Code specific events if available
const handleVisibilityChange = () => {
// Small delay to ensure VS Code has finished resizing
setTimeout(updateAvailableHeight, 100);
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
window.removeEventListener('resize', handleResize);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [updateAvailableHeight]);
// Send message to extension with promise-based response handling
const sendMessage = useCallback(
async (message: WebviewMessage): Promise => {
if (!vscode) {
throw new Error('VS Code API not available');
}
return new Promise((resolve, reject) => {
const requestId = `${Date.now()}-${Math.random()}`;
const messageWithId = { ...message, requestId };
// Set up timeout
const timeout = setTimeout(() => {
pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
}, 10000); // 10 second timeout
// Store the promise resolvers
pendingRequests.set(requestId, { resolve, reject, timeout });
// Send the message
vscode.postMessage(messageWithId);
});
},
[vscode, pendingRequests]
);
// Handle messages from extension
useEffect(() => {
if (!vscode) {
return;
}
const handleMessage = (event: MessageEvent) => {
const message: WebviewMessage = event.data;
console.log('π¨ Received message from extension:', message);
// Handle response to a pending request
if (message.requestId && pendingRequests.has(message.requestId)) {
const { resolve, reject, timeout } = pendingRequests.get(
message.requestId
)!;
clearTimeout(timeout);
pendingRequests.delete(message.requestId);
if (message.type === 'error') {
reject(new Error(message.error || 'Unknown error'));
} else {
resolve(message.data || message);
}
return;
}
// Handle different message types
switch (message.type) {
case 'init':
console.log('π Extension initialized:', message.data);
dispatch({
type: 'SET_CONNECTION_STATUS',
payload: { isConnected: true, status: 'Connected' }
});
break;
case 'tasksData':
console.log('π Received tasks data:', message.data);
dispatch({ type: 'SET_TASKS', payload: message.data });
break;
case 'taskStatusUpdated':
console.log('β
Task status updated:', message);
// Status is already updated optimistically, no need to update again
break;
case 'taskUpdated':
console.log('β
Task content updated:', message);
// Content is already updated optimistically, no need to update again
break;
case 'tasksUpdated':
console.log('π‘ Tasks updated from polling:', message);
// Only update if user is not currently interacting
if (!state.polling.isUserInteracting) {
dispatch({
type: 'TASKS_UPDATED_FROM_POLLING',
payload: message.data
});
dispatch({
type: 'SET_POLLING_STATUS',
payload: { isActive: true, errorCount: 0 }
});
} else {
console.log('βΈοΈ Skipping update due to user interaction');
}
break;
case 'pollingError':
console.log('β Polling error:', message);
dispatch({
type: 'SET_POLLING_STATUS',
payload: {
isActive: false,
errorCount: (state.polling.errorCount || 0) + 1
}
});
dispatch({
type: 'SET_ERROR',
payload: `Auto-refresh stopped: ${message.error}`
});
break;
case 'pollingStarted':
console.log('π Polling started');
dispatch({
type: 'SET_POLLING_STATUS',
payload: { isActive: true, errorCount: 0 }
});
break;
case 'pollingStopped':
console.log('βΉοΈ Polling stopped');
dispatch({
type: 'SET_POLLING_STATUS',
payload: { isActive: false }
});
break;
case 'connectionStatusUpdate':
console.log('π‘ Connection status update:', message);
dispatch({
type: 'SET_NETWORK_STATUS',
payload: {
isOfflineMode: message.data.isOfflineMode,
connectionStatus: message.data.status,
reconnectAttempts: message.data.reconnectAttempts,
maxReconnectAttempts: message.data.maxReconnectAttempts
}
});
break;
case 'networkOffline':
console.log('π Network offline, loading cached tasks:', message);
dispatch({
type: 'SET_NETWORK_STATUS',
payload: {
isOfflineMode: true,
connectionStatus: 'offline',
reconnectAttempts: message.data.reconnectAttempts,
lastSuccessfulConnection: message.data.lastSuccessfulConnection
}
});
// Load cached tasks if available
if (message.data.cachedTasks && message.data.cachedTasks.length > 0) {
dispatch({
type: 'LOAD_CACHED_TASKS',
payload: message.data.cachedTasks
});
}
break;
case 'reconnectionAttempted':
console.log('π Reconnection attempted:', message);
if (message.success) {
dispatch({
type: 'CLEAR_ERROR'
});
}
break;
case 'errorNotification':
console.log('β οΈ Error notification from extension:', message);
const errorData = message.data;
// Map error severity to toast type
let toastType: ToastNotification['type'] = 'error';
if (errorData.severity === 'low') {
toastType = 'info';
} else if (errorData.severity === 'medium') {
toastType = 'warning';
} else if (
errorData.severity === 'high' ||
errorData.severity === 'critical'
) {
toastType = 'error';
}
// Create appropriate toast based on error category
const title =
errorData.category === 'network'
? 'Network Error'
: errorData.category === 'mcp_connection'
? 'Connection Error'
: errorData.category === 'task_loading'
? 'Task Loading Error'
: errorData.category === 'ui_rendering'
? 'UI Error'
: 'Error';
dispatch({
type: 'ADD_TOAST',
payload: createToast(
toastType,
title,
errorData.message,
errorData.duration || (toastType === 'error' ? 8000 : 5000) // Use preference duration or fallback
)
});
break;
case 'error':
console.log('β General error from extension:', message);
const errorTitle =
message.errorType === 'connection' ? 'Connection Error' : 'Error';
const errorMessage = message.error || 'An unknown error occurred';
dispatch({
type: 'SET_ERROR',
payload: errorMessage
});
dispatch({
type: 'ADD_TOAST',
payload: createToast('error', errorTitle, errorMessage, 8000)
});
// Set offline mode for connection errors
if (message.errorType === 'connection') {
dispatch({
type: 'SET_NETWORK_STATUS',
payload: {
isOfflineMode: true,
connectionStatus: 'offline',
reconnectAttempts: 0
}
});
}
break;
case 'reactError':
console.log('π₯ React error reported to extension:', message);
// Show a toast for React errors too
dispatch({
type: 'ADD_TOAST',
payload: createToast(
'error',
'UI Error',
'A component error occurred. The extension may need to be reloaded.',
10000
)
});
break;
default:
console.log('β Unknown message type:', message.type);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [vscode, pendingRequests, state.polling]);
// Initialize the webview
useEffect(() => {
if (!vscode) {
console.warn('β οΈ VS Code API not available - running in standalone mode');
dispatch({
type: 'SET_CONNECTION_STATUS',
payload: { isConnected: false, status: 'Standalone Mode' }
});
return;
}
console.log('π Initializing webview...');
// Notify extension that webview is ready
vscode.postMessage({ type: 'ready' });
// Request initial tasks data
sendMessage({ type: 'getTasks' })
.then((tasksData) => {
console.log('π Initial tasks loaded:', tasksData);
dispatch({ type: 'SET_TASKS', payload: tasksData });
})
.catch((error) => {
console.error('β Failed to load initial tasks:', error);
dispatch({
type: 'SET_ERROR',
payload: `Failed to load tasks: ${error.message}`
});
});
}, [vscode, sendMessage]);
const contextValue = {
vscode,
state,
dispatch,
sendMessage,
availableHeight,
// Toast notification functions
showSuccessToast: showSuccessToast(dispatch),
showInfoToast: showInfoToast(dispatch),
showWarningToast: showWarningToast(dispatch),
showErrorToast: showErrorToast(dispatch)
};
return (
{
// Handle React errors and show appropriate toast
dispatch({
type: 'ADD_TOAST',
payload: createToast(
'error',
'Component Error',
`A React component crashed: ${error.message}`,
10000
)
});
}}
>
{/* Conditional rendering for different views */}
{(() => {
console.log(
'π― App render - currentView:',
state.currentView,
'selectedTaskId:',
state.selectedTaskId
);
return state.currentView === 'task-details' &&
state.selectedTaskId ? (
dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
onNavigateToTask={(taskId: string) =>
dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })
}
/>
) : (
);
})()}
dispatch({ type: 'REMOVE_TOAST', payload: id })}
/>
);
};
// Initialize React app
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render();
} else {
console.error('β Root container not found');
}