A powerful, feature-rich Kanban board component for managing task workflows with drag-and-drop, queue management, column customization, and real-time progress tracking.
Type: React Component
Location: apps/frontend/src/renderer/components/KanbanBoard.tsx
Category: UI Component
Status: Stable
Source of truth: Task types and status definitions are defined in
apps/frontend/src/shared/types/task.tsandapps/frontend/src/shared/constants/task.ts. Type definitions in this document mirror those canonical sources.
The KanbanBoard component provides a visual task management interface that displays tasks across six workflow stages (Backlog, Queue, In Progress, AI Review, Human Review, Done). It implements drag-and-drop task movement, automatic queue processing, multi-selection for bulk actions, and extensive column customization features including collapsing, resizing, and locking.
- Drag & Drop Task Management - Intuitive task movement between columns using @dnd-kit
- Automatic Queue System - Enforces parallel task limits with FIFO queue auto-promotion
- Column Customization - Collapse, resize, and lock columns with per-project persistence
- Multi-Selection & Bulk Actions - Select multiple tasks in Human Review for bulk PR creation
- Real-Time Progress Tracking - Shows task execution progress with skeleton loaders
- Archive Management - Archive completed tasks with toggle to show/hide archived items
- Worktree Integration - Handles worktree cleanup confirmation dialogs
- Smart Task Ordering - Persistent custom task ordering within columns
- Empty State Guidance - Contextual empty states with helpful hints for each column
- Task Workflow Management - Visualize and manage tasks through development lifecycle stages
- Parallel Work Control - Enforce limits on concurrent tasks with automatic queue processing
- Bulk Operations - Select and perform actions on multiple tasks simultaneously
- Column Organization - Customize board layout by collapsing unused columns or resizing active ones
- Task Status Transitions - Move tasks between workflow stages via drag-and-drop or status changes
- Archive Management - Keep completed tasks accessible but hidden from active workflow
import { KanbanBoard } from '@/renderer/components/KanbanBoard';Required:
react- Core React libraryreact-i18next- Internationalization (i18n) support@dnd-kit/core- Core drag-and-drop functionality@dnd-kit/sortable- Sortable list support for task reorderinglucide-react- Icon componentszustand- State management stores (task-store, project-store, kanban-settings-store)
Optional:
- None (all dependencies are required for full functionality)
| Prop | Type | Description | Example |
|---|---|---|---|
tasks |
Task[] |
Array of task objects to display | [{ id: '1', title: 'Implement login', ... }] |
onTaskClick |
(task: Task) => void |
Callback when a task card is clicked | (task) => navigate(\/tasks/${task.id}`)` |
| Prop | Type | Default | Description | Example |
|---|---|---|---|---|
onNewTaskClick |
() => void |
undefined |
Callback for "Add Task" button in Backlog column | () => setShowCreateDialog(true) |
onRefresh |
() => void |
undefined |
Callback for refresh button (enables refresh UI) | () => reloadTasks() |
isRefreshing |
boolean |
false |
Shows loading state during refresh | true |
| Event/Callback | Signature | Description | When Triggered |
|---|---|---|---|
onTaskClick |
(task: Task) => void |
Task card clicked | User clicks on a task card |
onNewTaskClick |
() => void |
Add task button clicked | User clicks "+" button in Backlog |
onRefresh |
() => void |
Refresh button clicked | User clicks refresh button in header |
The KanbanBoard internally manages these interactions through store updates:
| Handler | Purpose | Store/System |
|---|---|---|
handleDragStart |
Initiates drag operation | @dnd-kit/core |
handleDragOver |
Updates hover state during drag | @dnd-kit/core |
handleDragEnd |
Persists task status change | task-store |
handleStatusChange |
Updates task status with validation | task-store |
handleArchiveAll |
Archives all done tasks | task-store |
handleQueueAll |
Moves all backlog tasks to queue | task-store + queue system |
handleToggleColumnCollapsed |
Collapses/expands column | kanban-settings-store |
handleResizeStart/Move/End |
Resizes column width (mouse + touch) | kanban-settings-store |
handleToggleColumnLocked |
Locks/unlocks column from resizing | kanban-settings-store |
handleExpandAll |
Expands all collapsed columns at once | kanban-settings-store |
toggleTaskSelection |
Selects/deselects task for bulk actions | Local state |
selectAllTasks |
Selects all tasks in Human Review | Local state |
deselectAllTasks |
Clears all task selections | Local state |
processQueue |
Auto-promotes queued tasks (FIFO) with safety limits | task-store |
// Component props
interface KanbanBoardProps {
tasks: Task[];
onTaskClick: (task: Task) => void;
onNewTaskClick?: () => void;
onRefresh?: () => void;
isRefreshing?: boolean;
}
// Column props (internal to KanbanBoard)
interface DroppableColumnProps {
status: TaskStatus;
tasks: Task[];
onTaskClick: (task: Task) => void;
onStatusChange: (taskId: string, newStatus: TaskStatus) => unknown;
isOver: boolean;
onAddClick?: () => void;
onArchiveAll?: () => void;
onQueueSettings?: () => void;
onQueueAll?: () => void;
maxParallelTasks?: number;
archivedCount?: number;
showArchived?: boolean;
onToggleArchived?: () => void;
// Selection props for human_review column
selectedTaskIds?: Set<string>;
onSelectAll?: () => void;
onDeselectAll?: () => void;
onToggleSelect?: (taskId: string) => void;
// Collapse props
isCollapsed?: boolean;
onToggleCollapsed?: () => void;
// Resize props
columnWidth?: number;
isResizing?: boolean;
onResizeStart?: (startX: number) => void;
onResizeEnd?: () => void;
// Lock props
isLocked?: boolean;
onToggleLocked?: () => void;
// Loading state
isLoading?: boolean;
// Drag disabled when auto-sort is active
isDragDisabled?: boolean;
}
// Task type (from shared/types/task.ts)
interface Task {
id: string;
specId: string;
projectId: string;
title: string;
description: string;
status: TaskStatus;
reviewReason?: ReviewReason;
subtasks: Subtask[];
qaReport?: QAReport;
logs: string[];
metadata?: TaskMetadata;
executionProgress?: ExecutionProgress;
releasedInVersion?: string;
stagedInMainProject?: boolean;
stagedAt?: string;
location?: 'main' | 'worktree';
specsPath?: string;
tokenStats?: TaskTokenStats;
createdAt: Date;
updatedAt: Date;
}
// Task status (maps to Kanban columns)
type TaskStatus =
| 'backlog' // New tasks, not yet queued
| 'queue' // Waiting for available slot
| 'in_progress' // Currently executing
| 'ai_review' // QA validation in progress
| 'human_review' // Needs human attention
| 'done' // Completed
| 'pr_created' // PR created (shown in done column)
| 'error'; // Failed (shown in human_review column)
// Task order state (for custom ordering within columns)
type TaskOrderState = Record<TaskStatus, string[]>;
// Column preferences (persisted per project)
interface ColumnPreferences {
[columnId: string]: {
isCollapsed?: boolean;
width?: number;
isLocked?: boolean;
};
}The Kanban board displays six visual columns:
const TASK_STATUS_COLUMNS = [
'backlog',
'queue',
'in_progress',
'ai_review',
'human_review',
'done'
] as const;Note: Tasks with status pr_created are displayed in the done column, and tasks with status error are displayed in the human_review column (errors require human attention).
import { KanbanBoard } from '@/renderer/components/KanbanBoard';
import { useTaskStore } from '@/renderer/stores/task-store';
function TaskManagementView() {
const tasks = useTaskStore((state) => state.tasks);
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
return (
<div className="h-screen">
<KanbanBoard
tasks={tasks}
onTaskClick={(task) => setSelectedTask(task)}
/>
</div>
);
}function TaskManagementView() {
const tasks = useTaskStore((state) => state.tasks);
const [showCreateDialog, setShowCreateDialog] = useState(false);
return (
<>
<KanbanBoard
tasks={tasks}
onTaskClick={(task) => navigate(`/tasks/${task.id}`)}
onNewTaskClick={() => setShowCreateDialog(true)}
/>
{showCreateDialog && <TaskCreateDialog onClose={() => setShowCreateDialog(false)} />}
</>
);
}function TaskManagementView() {
const tasks = useTaskStore((state) => state.tasks);
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await window.api.tasks.refreshAll();
} finally {
setIsRefreshing(false);
}
};
return (
<KanbanBoard
tasks={tasks}
onTaskClick={(task) => setSelectedTask(task)}
onRefresh={handleRefresh}
isRefreshing={isRefreshing}
/>
);
}The KanbanBoard uses @dnd-kit for drag-and-drop functionality:
Features:
- 8px activation distance prevents accidental drags
- Drag overlay shows card preview during drag
- Drop zones highlight on hover
- Empty columns show drop target indicator
- Keyboard navigation support
- Automatically disabled when auto-sort is active (non-manual sort mode)
Task Movement:
- Drag between columns to change status
- Drag within column to reorder tasks
- Order persists per project
Queue System Integration:
- Dragging to "In Progress" when at capacity automatically queues the task
- Task order within columns uses custom ordering (newest first by default)
// Internal drag handling
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 }
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
);
// Drag end handler enforces queue limits
if (newStatus === 'in_progress' && inProgressCount >= maxParallelTasks) {
newStatus = 'queue'; // Auto-queue if at capacity
}The queue system enforces parallel task limits with automatic FIFO promotion:
How It Works:
- Project has configurable
maxParallelTasks(default: 3) - When In Progress is full, new tasks go to Queue
- When a slot opens, oldest queued task auto-promotes
- Manual drag to In Progress respects capacity
Queue Settings:
- Click settings icon in Queue column header
- Adjust
maxParallelTasksvalue - Persisted per project
// Queue processing (FIFO order) with safety limits
const processQueue = async () => {
if (isProcessingQueueRef.current) return; // Mutex: prevent concurrent executions
isProcessingQueueRef.current = true;
try {
const attemptedTaskIds = new Set<string>();
let consecutiveFailures = 0;
const MAX_CONSECUTIVE_FAILURES = 10;
while (true) {
const currentTasks = useTaskStore.getState().tasks;
const inProgressCount = currentTasks.filter(t => t.status === 'in_progress' && !t.metadata?.archivedAt).length;
const queuedTasks = currentTasks.filter(t =>
t.status === 'queue' && !t.metadata?.archivedAt && !attemptedTaskIds.has(t.id)
);
if (inProgressCount >= maxParallelTasks || queuedTasks.length === 0) break;
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) break;
const nextTask = queuedTasks.sort((a, b) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)[0];
const result = await persistTaskStatus(nextTask.id, 'in_progress');
if (result.success) {
consecutiveFailures = 0;
} else {
attemptedTaskIds.add(nextTask.id);
consecutiveFailures++;
}
}
} finally {
isProcessingQueueRef.current = false;
}
};Queue Actions:
- Queue All - Move all backlog tasks to queue (click ListPlus icon in Backlog)
- Settings - Configure max parallel tasks (click Settings icon in Queue)
Each column supports collapse, resize, and lock operations:
- Click chevron icon to collapse column to narrow strip
- Collapsed columns show rotated title and task count
- Click expand icon to restore column
- "Expand All" button appears when 3+ columns collapsed
// Collapse state persisted per project
const toggleColumnCollapsed = (status: TaskStatus) => {
kanbanSettingsStore.toggleColumnCollapsed(status);
kanbanSettingsStore.savePreferences(projectId);
};- Drag right edge of column to resize (mouse and touch supported)
- Width constrained between
MIN_COLUMN_WIDTH(180px) andMAX_COLUMN_WIDTH(600px) - Resize persisted per project
- Cannot resize when column is locked
- Touch events use
touch-noneCSS to prevent scroll interference - ProjectId captured at resize start to avoid stale closures in save callback
// Column width constants (from kanban-settings-store)
const MIN_COLUMN_WIDTH = 180;
const MAX_COLUMN_WIDTH = 600;
const DEFAULT_COLUMN_WIDTH = 320;
const COLLAPSED_COLUMN_WIDTH = 48;- Click lock icon to prevent accidental resizing
- Locked columns show amber highlight on resize handle
- Lock state persisted per project
The Human Review column supports multi-selection for bulk operations:
Selection UI:
- Checkbox in column header for select all / deselect all
- Shows indeterminate state when some selected
- Each task card has selection checkbox
- Floating action bar shows selected count
Bulk Actions:
- Create PRs - Opens bulk PR dialog for selected tasks
- Clear Selection - Deselects all tasks
// Selection state
const [selectedTaskIds, setSelectedTaskIds] = useState<Set<string>>(new Set());
// Bulk PR creation
const handleOpenBulkPRDialog = () => {
if (selectedTaskIds.size > 0) {
setBulkPRDialogOpen(true);
}
};Floating Action Bar: Appears at bottom-center when tasks are selected, showing:
- Selected count
- "Create PRs" button
- "Clear Selection" button
Done column includes archive functionality:
Features:
- Archive All - Move all done tasks to archived state
- Toggle Show Archived - Show/hide archived tasks (button shows count)
- Archived tasks have
metadata.archivedAttimestamp - Archive state filters tasks across entire board
// Archive all done tasks
const handleArchiveAll = async () => {
const doneTaskIds = tasksByStatus.done.map((t) => t.id);
await archiveTasks(projectId, doneTaskIds);
};
// Filter tasks based on archive visibility
const filteredTasks = showArchived
? tasks
: tasks.filter((t) => !t.metadata?.archivedAt);Each column shows contextual empty state when no tasks:
- Icon - Column-specific icon (Inbox, Loader, Eye, CheckCircle)
- Message - Encouraging message (e.g., "No tasks in backlog")
- Subtext - Helpful hint (e.g., "Create a task to get started")
- Drop Indicator - Shows "Drop here" message during drag-over
Empty states use i18n keys from tasks:kanban.empty* namespace.
When moving a task to Done that has an active worktree:
- Status change fails with worktree conflict
WorktreeCleanupDialogappears- User confirms to force-complete and clean up worktree
- Task moves to Done and worktree is removed
// Status change with worktree handling
const handleStatusChange = async (taskId: string, newStatus: TaskStatus) => {
const result = await persistTaskStatus(taskId, newStatus);
if (!result.success && result.worktreeExists) {
// Show cleanup confirmation dialog
setWorktreeCleanupDialog({
open: true,
taskId,
worktreePath: result.worktreePath,
// ...
});
}
};Tasks within columns maintain custom order:
Ordering Strategy:
- Load persisted order from
taskOrderstate - New tasks (not in order) appear at top
- Dragging within column updates order
- Order persists per project via
task-store - Stale IDs (deleted tasks) pruned automatically
// Task ordering with new tasks at top
const validOrder = columnOrder.filter(id => currentTaskIds.has(id));
const newTasks = columnTasks.filter(t => !validOrderSet.has(t.id));
// New tasks sorted by createdAt (newest first)
newTasks.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// Final order: new tasks, then ordered tasks
grouped[status] = [...newTasks, ...orderedTasks];The board header includes a KanbanFilters component for search, sort, and filter:
KanbanFilters Component (KanbanFilters.tsx):
interface KanbanFiltersProps {
projectId: string | undefined;
}Features:
- Search - Filter tasks by title/description (debounced 300ms save)
- Sort mode - Choose from
manual,priority,created,updated - Sort order - Toggle ascending/descending (hidden when sort is
manual) - Clear filters - Reset all filters with active filter count badge
- All filter state managed via
useKanbanSettingsStoreand persisted to localStorage
useTaskFiltering Hook (hooks/useTaskFiltering.ts):
const {
filteredTasks, // Tasks after applying search and filters
filterState, // Current filter state
hasActiveFilters, // Whether any filter is active
setSearchQuery, // Set search text
clearFilters // Reset all filters
} = useTaskFiltering(tasks);When a non-manual sort mode is selected, drag-and-drop is automatically disabled:
const isAutoSortActive = useMemo(() => {
const sortBy = filters?.sortBy ?? 'manual';
return sortBy !== 'manual';
}, [filters?.sortBy]);
// Drag handlers disabled when auto-sort active
onDragStart={isAutoSortActive ? undefined : handleDragStart}When isAutoSortActive is true, the isDragDisabled prop is passed to DroppableColumn and SortableTaskCard, which shows a tooltip explaining that drag is disabled while auto-sort is active.
Responsibilities:
- Provides
tasksarray - Persists task status changes via
persistTaskStatus() - Manages task order via
reorderTasksInColumn()andmoveTaskToColumnTop() - Handles archive operations via
archiveTasks() - Registers status change listeners for queue processing
import { useTaskStore, persistTaskStatus } from '@/renderer/stores/task-store';
// Get tasks
const tasks = useTaskStore((state) => state.tasks);
// Change task status
await persistTaskStatus(taskId, 'done');
// Reorder tasks within column
reorderTasksInColumn('backlog', draggedTaskId, targetTaskId);Responsibilities:
- Provides project settings (maxParallelTasks)
- Updates queue settings via
updateProjectSettings()
import { useProjectStore, updateProjectSettings } from '@/renderer/stores/project-store';
// Get max parallel tasks
const projects = useProjectStore((state) => state.projects);
const project = projects.find((p) => p.id === projectId);
const maxParallelTasks = project?.settings?.maxParallelTasks ?? 3;
// Update queue settings
await updateProjectSettings(projectId, { maxParallelTasks: 5 });Responsibilities:
- Manages column preferences (collapsed, width, locked)
- Manages filter/sort state (search query, sort mode, sort order)
- Persists preferences and filters per project
- Provides constants for column dimensions
import {
useKanbanSettingsStore,
COLLAPSED_COLUMN_WIDTH,
DEFAULT_COLUMN_WIDTH,
MIN_COLUMN_WIDTH,
MAX_COLUMN_WIDTH
} from '@/renderer/stores/kanban-settings-store';
// Get column preferences
const columnPreferences = useKanbanSettingsStore((state) => state.columnPreferences);
const isCollapsed = columnPreferences?.['backlog']?.isCollapsed;
const width = columnPreferences?.['backlog']?.width ?? DEFAULT_COLUMN_WIDTH;
// Toggle collapsed state
toggleColumnCollapsed('backlog');
savePreferences(projectId);Responsibilities:
- Manages global view state (showArchived)
- Provides
toggleShowArchived()action
import { useViewState } from '@/renderer/contexts/ViewStateContext';
const { showArchived, toggleShowArchived } = useViewState();DroppableColumn uses React.memo() with custom comparator:
- Shallow comparison of simple props
- Deep comparison of tasks array using
tasksAreEquivalent() - Reference equality for function props
- Set comparison for
selectedTaskIds
const DroppableColumn = memo(function DroppableColumn(props) {
// ...
}, droppableColumnPropsAreEqual);Task card handlers cached in useRef Maps:
onClickHandlers- Click handlers per taskonStatusChangeHandlers- Status change handlers per taskonToggleSelectHandlers- Selection toggle handlers per task
Updated only when tasks or callbacks change:
useEffect(() => {
const clickHandlers = new Map<string, () => void>();
tasks.forEach((task) => {
clickHandlers.set(task.id, () => onTaskClick(task));
});
onClickHandlers.current = clickHandlers;
}, [tasks, onTaskClick]);Task card elements memoized to prevent recreation:
const taskCards = useMemo(() => {
return tasks.map((task) => (
<SortableTaskCard
key={task.id}
task={task}
onClick={onClickHandlers.current.get(task.id)}
onStatusChange={onStatusChangeHandlers.current.get(task.id)}
// ...
/>
));
}, [tasks, selectedTaskIds, isDragDisabled]);tasksByStatus uses optimized sorting:
// Pre-compute index map for O(n) sorting instead of O(n²)
const indexMap = new Map(validOrder.map((id, idx) => [id, idx]));
const orderedTasks = columnTasks
.filter(t => validOrderSet.has(t.id))
.sort((a, b) => (indexMap.get(a.id) ?? 0) - (indexMap.get(b.id) ?? 0));- Column headers have semantic headings
- Buttons have
aria-labelattributes - Checkboxes have
aria-labelfor screen readers - Lock toggle has
aria-pressedstate - Collapse toggle has
aria-labelwith dynamic text
- Full keyboard support via
@dnd-kitsensors - Tab navigation through interactive elements
- Enter/Space to activate buttons
- Arrow keys for drag-and-drop navigation
- Focus indicators on interactive elements
- Hover states for drag handles and buttons
- Color-coded status badges
- Loading skeletons during refresh
All user-facing text uses react-i18next translation keys:
tasks- Task-related content (column labels, empty states, actions)dialogs- Dialog content (worktree cleanup, queue settings)common- Common terms (buttons, errors, accessibility)
// Column labels
t(TASK_STATUS_LABELS[status]) // 'columns.backlog'
// Empty states
t('kanban.emptyBacklog') // "No tasks in backlog"
t('kanban.emptyBacklogHint') // "Create a task to get started"
// Actions
t('kanban.addTaskAriaLabel') // "Add new task"
t('kanban.queueSettings') // "Queue settings"
t('kanban.archiveAllDone') // "Archive all completed tasks"
// Selection
t('kanban.selectAll') // "Select all"
t('kanban.selectedCountOther', { count: 5 }) // "5 tasks selected"Symptom: Tasks stay in queue when slots available
Causes:
processQueue()not registered as status change listener- Race condition from concurrent processing
- Stale task state in memoized values
Solution:
- Ensure
useEffectregisters listener on mount - Use
isProcessingQueueRefto prevent concurrent execution - Get current state from store:
useTaskStore.getState().tasks
Symptom: Column resets to default width on reload
Causes:
projectIdisundefinedwhen saving- Preferences not loaded on mount
- Save called before state update completes
Solution:
- Capture
projectIdin ref at resize start - Load preferences in
useEffectwhenprojectIdchanges - Use
setTimeoutto ensure state update before save
Symptom: Selected tasks deselect unexpectedly
Causes:
- Tasks move out of human_review column
- Component remounts
- Selection state not cleaned up
Solution:
- Prune stale IDs in
useEffectwhentasksByStatus.human_reviewchanges - Store selection in component state (not persisted)
- Clear selection after bulk actions complete
Symptom: Tasks can't be dragged
Causes:
- Task IDs not unique
SortableContextitems not memoized- Activation constraint too strict
Solution:
- Ensure each task has unique
idproperty - Memoize
taskIdsarray to prevent context re-render - Adjust
activationConstraint.distanceif needed
- TaskCard - Individual task card component
- SortableTaskCard - Draggable wrapper for TaskCard
- TaskCardSkeleton - Loading skeleton for task cards
- QueueSettingsModal - Modal for configuring queue settings
- WorktreeCleanupDialog - Confirmation dialog for worktree cleanup
- BulkPRDialog - Dialog for bulk PR creation
- task-store - Task data and operations
- project-store - Project settings and configuration
- kanban-settings-store - Column preferences (collapse, width, lock)
- Task Types - TypeScript type definitions
- Task Constants - Status, labels, colors
- Task Store - State management
- @dnd-kit Documentation - Drag-and-drop library
- v1.0 - Initial implementation with drag-and-drop
- v1.1 - Added queue system with auto-promotion
- v1.2 - Added column collapse/expand functionality
- v1.3 - Added column resize with persistence
- v1.4 - Added column lock to prevent accidental resize
- v1.5 - Added multi-selection and bulk PR creation
- v1.6 - Performance optimizations (memoization, stable handlers)
- v1.7 - Added worktree cleanup confirmation dialog
- v1.8 - Added "Expand All" button for collapsed columns