Files
Mike Swanson 75ce1c2fd5 feat: Add Sequential Thinking to Code Review + Frontend Validation
Enhanced code review and frontend validation with intelligent triggers:

Code Review Agent Enhancement:
- Added Sequential Thinking MCP integration for complex issues
- Triggers on 2+ rejections or 3+ critical issues
- New escalation format with root cause analysis
- Comprehensive solution strategies with trade-off evaluation
- Educational feedback to break rejection cycles
- Files: .claude/agents/code-review.md (+308 lines)
- Docs: CODE_REVIEW_ST_ENHANCEMENT.md, CODE_REVIEW_ST_TESTING.md

Frontend Design Skill Enhancement:
- Automatic invocation for ANY UI change
- Comprehensive validation checklist (200+ checkpoints)
- 8 validation categories (visual, interactive, responsive, a11y, etc.)
- 3 validation levels (quick, standard, comprehensive)
- Integration with code review workflow
- Files: .claude/skills/frontend-design/SKILL.md (+120 lines)
- Docs: UI_VALIDATION_CHECKLIST.md (462 lines), AUTOMATIC_VALIDATION_ENHANCEMENT.md (587 lines)

Settings Optimization:
- Repaired .claude/settings.local.json (fixed m365 pattern)
- Reduced permissions from 49 to 33 (33% reduction)
- Removed duplicates, sorted alphabetically
- Created SETTINGS_PERMISSIONS.md documentation

Checkpoint Command Enhancement:
- Dual checkpoint system (git + database)
- Saves session context to API for cross-machine recall
- Includes git metadata in database context
- Files: .claude/commands/checkpoint.md (+139 lines)

Decision Rationale:
- Sequential Thinking MCP breaks rejection cycles by identifying root causes
- Automatic frontend validation catches UI issues before code review
- Dual checkpoints enable complete project memory across machines
- Settings optimization improves maintainability

Total: 1,200+ lines of documentation and enhancements

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 16:23:52 -07:00

935 lines
48 KiB
Plaintext

1→import { useEffect, useState, useMemo } from 'react'
2→import { useSearchParams } from 'react-router-dom'
3→import { Plus, X, GripVertical, Clock, User, RefreshCw, Flag, Pencil, Check, Trash2, Filter, ArrowUpDown, LayoutGrid, List, ChevronLeft, ChevronRight } from 'lucide-react'
4→import * as Dialog from '@radix-ui/react-dialog'
5→import * as AlertDialog from '@radix-ui/react-alert-dialog'
6→import { useProjectStore } from '../stores/projectStore'
7→import { useTaskStore, Task, TaskCreate, TaskUpdate } from '../stores/taskStore'
8→
9→const ITEMS_PER_PAGE = 10
10→
11→const COLUMNS = [
12→ { id: 'backlog', title: 'Backlog', color: 'border-slate-500' },
13→ { id: 'in_progress', title: 'In Progress', color: 'border-accent' },
14→ { id: 'qa', title: 'QA', color: 'border-primary-400' },
15→ { id: 'done', title: 'Done', color: 'border-green-400' },
16→ { id: 'blocked', title: 'Blocked', color: 'border-destructive' },
17→]
18→
19→const PRIORITY_LABELS: Record<number, string> = {
20→ 0: 'Low',
21→ 1: 'Medium',
22→ 2: 'High',
23→ 3: 'Critical',
24→}
25→
26→const STATUS_LABELS: Record<string, string> = {
27→ backlog: 'Backlog',
28→ in_progress: 'In Progress',
29→ qa: 'QA',
30→ done: 'Done',
31→ blocked: 'Blocked',
32→}
33→
34→export default function KanbanBoard() {
35→ const [searchParams, setSearchParams] = useSearchParams()
36→ const { currentProject } = useProjectStore()
37→ const { tasks, fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask, isTaskDeleting, isLoading } = useTaskStore()
38→ const [showAddDialog, setShowAddDialog] = useState(false)
39→ const [newTaskName, setNewTaskName] = useState('')
40→ const [newTaskDescription, setNewTaskDescription] = useState('')
41→ const [newTaskPriority, setNewTaskPriority] = useState(0)
42→ const [isCreating, setIsCreating] = useState(false)
43→ const [draggedTask, setDraggedTask] = useState<number | null>(null)
44→ const [selectedTask, setSelectedTask] = useState<Task | null>(null)
45→ const [isEditing, setIsEditing] = useState(false)
46→ const [editName, setEditName] = useState('')
47→ const [editDescription, setEditDescription] = useState('')
48→ const [editPriority, setEditPriority] = useState(0)
49→ const [isSaving, setIsSaving] = useState(false)
50→ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
51→ const [isDeleting, setIsDeleting] = useState(false)
52→ const [showUnsavedDialog, setShowUnsavedDialog] = useState(false)
53→ const [pendingCloseAction, setPendingCloseAction] = useState<'close' | 'cancel' | null>(null)
54→ // Filter and Sort state - initialize from URL params
55→ const getInitialPriorityFilter = (): number | 'all' => {
56→ const param = searchParams.get('priority')
57→ if (param === 'all' || param === null) return 'all'
58→ const num = parseInt(param, 10)
59→ return [0, 1, 2, 3].includes(num) ? num : 'all'
60→ }
61→ const getInitialSortOrder = (): 'newest' | 'oldest' | 'priority' => {
62→ const param = searchParams.get('sort')
63→ if (param === 'newest' || param === 'oldest' || param === 'priority') return param
64→ return 'newest'
65→ }
66→ const getInitialViewMode = (): 'kanban' | 'list' => {
67→ const param = searchParams.get('view')
68→ if (param === 'list') return 'list'
69→ return 'kanban'
70→ }
71→
72→ const [priorityFilter, setPriorityFilter] = useState<number | 'all'>(getInitialPriorityFilter)
73→ const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority'>(getInitialSortOrder)
74→ // View mode and pagination state
75→ const [viewMode, setViewMode] = useState<'kanban' | 'list'>(getInitialViewMode)
76→ const [currentPage, setCurrentPage] = useState(1)
77→
78→ useEffect(() => {
79→ if (currentProject?.id) {
80→ fetchTasks(currentProject.id)
81→ }
82→ }, [currentProject?.id, fetchTasks])
83→
84→ // Handle deep linking - select task from URL param after tasks are loaded
85→ useEffect(() => {
86→ const taskIdParam = searchParams.get('task')
87→ if (taskIdParam && tasks.length > 0) {
88→ const taskId = parseInt(taskIdParam, 10)
89→ const task = tasks.find(t => t.id === taskId)
90→ if (task) {
91→ setSelectedTask(task)
92→ }
93→ }
94→ }, [searchParams, tasks])
95→
96→ // Update URL when task is selected
97→ const handleSelectTask = (task: Task) => {
98→ setSelectedTask(task)
99→ const newParams = new URLSearchParams(searchParams)
100→ newParams.set('task', task.id.toString())
101→ setSearchParams(newParams)
102→ }
103→
104→ // Clear task from URL when panel is closed
105→ const clearTaskFromUrl = () => {
106→ const newParams = new URLSearchParams(searchParams)
107→ newParams.delete('task')
108→ setSearchParams(newParams)
109→ }
110→
111→ // Update URL when filter changes
112→ const handlePriorityFilterChange = (value: number | 'all') => {
113→ setPriorityFilter(value)
114→ const newParams = new URLSearchParams(searchParams)
115→ if (value === 'all') {
116→ newParams.delete('priority')
117→ } else {
118→ newParams.set('priority', value.toString())
119→ }
120→ setSearchParams(newParams)
121→ }
122→
123→ // Update URL when sort changes
124→ const handleSortOrderChange = (value: 'newest' | 'oldest' | 'priority') => {
125→ setSortOrder(value)
126→ const newParams = new URLSearchParams(searchParams)
127→ if (value === 'newest') {
128→ newParams.delete('sort') // default value, no need to store
129→ } else {
130→ newParams.set('sort', value)
131→ }
132→ setSearchParams(newParams)
133→ }
134→
135→ // Update URL when view mode changes
136→ const handleViewModeChange = (value: 'kanban' | 'list') => {
137→ setViewMode(value)
138→ const newParams = new URLSearchParams(searchParams)
139→ if (value === 'kanban') {
140→ newParams.delete('view') // default value, no need to store
141→ } else {
142→ newParams.set('view', value)
143→ }
144→ setSearchParams(newParams)
145→ }
146→
147→ // Filtered and sorted tasks
148→ const filteredAndSortedTasks = useMemo(() => {
149→ let result = [...tasks]
150→
151→ // Apply priority filter
152→ if (priorityFilter !== 'all') {
153→ result = result.filter(task => task.priority === priorityFilter)
154→ }
155→
156→ // Apply sort
157→ result.sort((a, b) => {
158→ if (sortOrder === 'newest') {
159→ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
160→ } else if (sortOrder === 'oldest') {
161→ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
162→ } else {
163→ // priority - highest first
164→ return b.priority - a.priority
165→ }
166→ })
167→
168→ return result
169→ }, [tasks, priorityFilter, sortOrder])
170→
171→ // Pagination calculations
172→ const totalPages = Math.ceil(filteredAndSortedTasks.length / ITEMS_PER_PAGE)
173→ const paginatedTasks = useMemo(() => {
174→ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
175→ return filteredAndSortedTasks.slice(startIndex, startIndex + ITEMS_PER_PAGE)
176→ }, [filteredAndSortedTasks, currentPage])
177→
178→ // Reset page when filter changes
179→ useEffect(() => {
180→ setCurrentPage(1)
181→ }, [priorityFilter, sortOrder])
182→
183→ const getTasksByStatus = (status: string) => {
184→ return filteredAndSortedTasks.filter((task) => task.status === status)
185→ }
186→
187→ const handleCreateTask = async () => {
188→ if (!currentProject || !newTaskName.trim()) return
189→ setIsCreating(true)
190→ try {
191→ const taskData: TaskCreate = {
192→ name: newTaskName.trim(),
193→ description: newTaskDescription.trim() || undefined,
194→ priority: newTaskPriority,
195→ }
196→ await createTask(currentProject.id, taskData)
197→ setNewTaskName('')
198→ setNewTaskDescription('')
199→ setNewTaskPriority(0)
200→ setShowAddDialog(false)
201→ } catch (error) {
202→ console.error('Failed to create task:', error)
203→ } finally {
204→ setIsCreating(false)
205→ }
206→ }
207→
208→ const handleDragStart = (taskId: number) => {
209→ setDraggedTask(taskId)
210→ }
211→
212→ const handleDragOver = (e: React.DragEvent) => {
213→ e.preventDefault()
214→ }
215→
216→ const handleDrop = async (targetStatus: string) => {
217→ if (draggedTask === null) return
218→ try {
219→ await updateTaskStatus(draggedTask, targetStatus)
220→ } catch (error) {
221→ console.error('Failed to update task status:', error)
222→ }
223→ setDraggedTask(null)
224→ }
225→
226→ const handleStartEdit = () => {
227→ if (!selectedTask) return
228→ setEditName(selectedTask.name)
229→ setEditDescription(selectedTask.description || '')
230→ setEditPriority(selectedTask.priority)
231→ setIsEditing(true)
232→ }
233→
234→ // Check if edit form has unsaved changes
235→ const hasUnsavedChanges = () => {
236→ if (!selectedTask || !isEditing) return false
237→ return (
238→ editName.trim() !== selectedTask.name ||
239→ (editDescription.trim() || '') !== (selectedTask.description || '') ||
240→ editPriority !== selectedTask.priority
241→ )
242→ }
243→
244→ const handleCancelEdit = () => {
245→ if (hasUnsavedChanges()) {
246→ setPendingCloseAction('cancel')
247→ setShowUnsavedDialog(true)
248→ } else {
249→ setIsEditing(false)
250→ }
251→ }
252→
253→ const handleClosePanel = () => {
254→ if (hasUnsavedChanges()) {
255→ setPendingCloseAction('close')
256→ setShowUnsavedDialog(true)
257→ } else {
258→ setSelectedTask(null)
259→ setIsEditing(false)
260→ clearTaskFromUrl()
261→ }
262→ }
263→
264→ const handleDiscardChanges = () => {
265→ setShowUnsavedDialog(false)
266→ if (pendingCloseAction === 'close') {
267→ setSelectedTask(null)
268→ clearTaskFromUrl()
269→ }
270→ setIsEditing(false)
271→ setPendingCloseAction(null)
272→ }
273→
274→ const handleKeepEditing = () => {
275→ setShowUnsavedDialog(false)
276→ setPendingCloseAction(null)
277→ }
278→
279→ const handleSaveEdit = async () => {
280→ if (!selectedTask) return
281→ setIsSaving(true)
282→ try {
283→ const updates: TaskUpdate = {
284→ name: editName.trim(),
285→ description: editDescription.trim() || undefined,
286→ priority: editPriority,
287→ }
288→ const updatedTask = await updateTask(selectedTask.id, updates)
289→ setSelectedTask(updatedTask)
290→ setIsEditing(false)
291→ } catch (error) {
292→ console.error('Failed to update task:', error)
293→ } finally {
294→ setIsSaving(false)
295→ }
296→ }
297→
298→ const handleDeleteTask = async () => {
299→ if (!selectedTask) return
300→ // Check if already being deleted (prevents rapid clicks)
301→ if (isTaskDeleting(selectedTask.id)) return
302→
303→ const taskIdToDelete = selectedTask.id
304→ setIsDeleting(true)
305→ // Close dialog immediately to prevent additional clicks
306→ setShowDeleteDialog(false)
307→
308→ try {
309→ await deleteTask(taskIdToDelete)
310→ setSelectedTask(null)
311→ clearTaskFromUrl()
312→ } catch (error) {
313→ console.error('Failed to delete task:', error)
314→ // Re-open dialog if delete failed so user can retry
315→ setShowDeleteDialog(true)
316→ } finally {
317→ setIsDeleting(false)
318→ }
319→ }
320→
321→ if (!currentProject) {
322→ return (
323→ <div className="flex items-center justify-center h-full">
324→ <p className="text-slate-500">Select a project to view its Kanban board</p>
325→ </div>
326→ )
327→ }
328→
329→ return (
330→ <div className="h-full flex flex-col">
331→ {/* Header */}
332→ <div className="flex items-center justify-between mb-6">
333→ <h1 className="text-2xl font-bold">Kanban Board</h1>
334→ <div className="flex items-center gap-4">
335→ {/* Priority Filter */}
336→ <div className="flex items-center gap-2">
337→ <Filter size={16} className="text-slate-400" />
338→ <select
339→ value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
340→ onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
341→ className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
342→ >
343→ <option value="all">All Priorities</option>
344→ <option value="3">Critical</option>
345→ <option value="2">High</option>
346→ <option value="1">Medium</option>
347→ <option value="0">Low</option>
348→ </select>
349→ </div>
350→
351→ {/* Sort Order */}
352→ <div className="flex items-center gap-2">
353→ <ArrowUpDown size={16} className="text-slate-400" />
354→ <select
355→ value={sortOrder}
356→ onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
357→ className="px-3 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white focus:outline-none focus:ring-2 focus:ring-accent"
358→ >
359→ <option value="newest">Newest First</option>
360→ <option value="oldest">Oldest First</option>
361→ <option value="priority">By Priority</option>
362→ </select>
363→ </div>
364→
365→ {/* View Mode Toggle */}
366→ <div className="flex items-center gap-1 bg-slate-800 border border-slate-700 rounded-lg p-1">
367→ <button
368→ onClick={() => handleViewModeChange('kanban')}
369→ className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
370→ title="Kanban View"
371→ >
372→ <LayoutGrid size={18} />
373→ </button>
374→ <button
375→ onClick={() => handleViewModeChange('list')}
376→ className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-slate-400 hover:text-white'}`}
377→ title="List View"
378→ >
379→ <List size={18} />
380→ </button>
381→ </div>
382→
383→ <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
384→ <Dialog.Trigger asChild>
385→ <button className="btn-accent flex items-center gap-2">
386→ <Plus size={18} />
387→ Add Task
388→ </button>
389→ </Dialog.Trigger>
390→ <Dialog.Portal>
391→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
392→ <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
393→ <div className="flex items-center justify-between mb-4">
394→ <Dialog.Title className="text-xl font-semibold text-white">
395→ Add New Task
396→ </Dialog.Title>
397→ <Dialog.Close asChild>
398→ <button className="text-slate-400 hover:text-white">
399→ <X size={20} />
400→ </button>
401→ </Dialog.Close>
402→ </div>
403→ <Dialog.Description className="text-slate-400 mb-4">
404→ Create a new task for this project.
405→ </Dialog.Description>
406→ <div className="space-y-4">
407→ <div>
408→ <label className="block text-sm font-medium text-slate-300 mb-1">
409→ Task Name *
410→ </label>
411→ <input
412→ type="text"
413→ value={newTaskName}
414→ onChange={(e) => setNewTaskName(e.target.value)}
415→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
416→ placeholder="Enter task name"
417→ />
418→ </div>
419→ <div>
420→ <label className="block text-sm font-medium text-slate-300 mb-1">
421→ Description
422→ </label>
423→ <textarea
424→ value={newTaskDescription}
425→ onChange={(e) => setNewTaskDescription(e.target.value)}
426→ rows={3}
427→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
428→ placeholder="Enter task description"
429→ />
430→ </div>
431→ <div>
432→ <label className="block text-sm font-medium text-slate-300 mb-1">
433→ Priority
434→ </label>
435→ <select
436→ value={newTaskPriority}
437→ onChange={(e) => setNewTaskPriority(Number(e.target.value))}
438→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
439→ >
440→ <option value={0}>Low</option>
441→ <option value={1}>Medium</option>
442→ <option value={2}>High</option>
443→ <option value={3}>Critical</option>
444→ </select>
445→ </div>
446→ </div>
447→ <div className="mt-6 flex justify-end gap-3">
448→ <Dialog.Close asChild>
449→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
450→ Cancel
451→ </button>
452→ </Dialog.Close>
453→ <button
454→ onClick={handleCreateTask}
455→ disabled={isCreating || !newTaskName.trim()}
456→ className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
457→ >
458→ {isCreating ? 'Creating...' : 'Create Task'}
459→ </button>
460→ </div>
461→ </Dialog.Content>
462→ </Dialog.Portal>
463→ </Dialog.Root>
464→ </div>
465→ </div>
466→
467→ {/* Main Content Area */}
468→ <div className="flex-1 flex gap-4 overflow-hidden">
469→ {viewMode === 'list' ? (
470→ /* List View with Pagination */
471→ <div className="flex-1 flex flex-col">
472→ {/* Task List Table */}
473→ <div className="flex-1 overflow-auto bg-slate-800 rounded-lg border border-slate-700">
474→ <table className="w-full">
475→ <thead className="bg-slate-900 sticky top-0">
476→ <tr className="text-left text-sm text-slate-400">
477→ <th className="px-4 py-3 font-medium">Task Name</th>
478→ <th className="px-4 py-3 font-medium">Status</th>
479→ <th className="px-4 py-3 font-medium">Priority</th>
480→ <th className="px-4 py-3 font-medium">Created</th>
481→ </tr>
482→ </thead>
483→ <tbody>
484→ {isLoading && tasks.length === 0 ? (
485→ <tr>
486→ <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
487→ Loading...
488→ </td>
489→ </tr>
490→ ) : paginatedTasks.length === 0 ? (
491→ <tr>
492→ <td colSpan={4} className="px-4 py-8 text-center text-slate-500">
493→ No tasks found
494→ </td>
495→ </tr>
496→ ) : (
497→ paginatedTasks.map((task) => (
498→ <tr
499→ key={task.id}
500→ onClick={() => handleSelectTask(task)}
501→ className={`border-t border-slate-700 hover:bg-slate-700/50 cursor-pointer ${
502→ selectedTask?.id === task.id ? 'bg-slate-700' : ''
503→ }`}
504→ >
505→ <td className="px-4 py-3">
506→ <div className="font-medium">{task.name}</div>
507→ {task.description && (
508→ <div className="text-sm text-slate-400 truncate max-w-md">
509→ {task.description}
510→ </div>
511→ )}
512→ </td>
513→ <td className="px-4 py-3">
514→ <span className={`badge ${
515→ task.status === 'backlog' ? 'badge-backlog' :
516→ task.status === 'in_progress' ? 'badge-in-progress' :
517→ task.status === 'qa' ? 'badge-qa' :
518→ task.status === 'done' ? 'badge-done' :
519→ 'badge-blocked'
520→ }`}>
521→ {STATUS_LABELS[task.status] || task.status}
522→ </span>
523→ </td>
524→ <td className="px-4 py-3">
525→ <span className={`text-sm ${
526→ task.priority === 3 ? 'text-destructive' :
527→ task.priority === 2 ? 'text-warning' :
528→ task.priority === 1 ? 'text-accent' :
529→ 'text-slate-400'
530→ }`}>
531→ {PRIORITY_LABELS[task.priority] || 'Unknown'}
532→ </span>
533→ </td>
534→ <td className="px-4 py-3 text-sm text-slate-400">
535→ {new Date(task.created_at).toLocaleDateString()}
536→ </td>
537→ </tr>
538→ ))
539→ )}
540→ </tbody>
541→ </table>
542→ </div>
543→
544→ {/* Pagination Controls */}
545→ {totalPages > 1 && (
546→ <div className="flex items-center justify-between mt-4 px-2">
547→ <div className="text-sm text-slate-400">
548→ Showing {((currentPage - 1) * ITEMS_PER_PAGE) + 1} to {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedTasks.length)} of {filteredAndSortedTasks.length} tasks
549→ </div>
550→ <div className="flex items-center gap-2">
551→ <button
552→ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
553→ disabled={currentPage === 1}
554→ className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
555→ >
556→ <ChevronLeft size={18} />
557→ </button>
558→ <div className="flex items-center gap-1">
559→ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
560→ <button
561→ key={page}
562→ onClick={() => setCurrentPage(page)}
563→ className={`w-8 h-8 rounded-lg text-sm ${
564→ page === currentPage
565→ ? 'bg-accent text-white'
566→ : 'bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700'
567→ }`}
568→ >
569→ {page}
570→ </button>
571→ ))}
572→ </div>
573→ <button
574→ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
575→ disabled={currentPage === totalPages}
576→ className="p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-400 hover:text-white hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed"
577→ >
578→ <ChevronRight size={18} />
579→ </button>
580→ </div>
581→ </div>
582→ )}
583→ </div>
584→ ) : (
585→ /* Kanban Columns */
586→ <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
587→ {COLUMNS.map((column) => (
588→ <div
589→ key={column.id}
590→ className={`flex-shrink-0 w-72 bg-slate-800 rounded-lg border-t-2 ${column.color}`}
591→ onDragOver={handleDragOver}
592→ onDrop={() => handleDrop(column.id)}
593→ >
594→ {/* Column Header */}
595→ <div className="p-4 border-b border-slate-700">
596→ <div className="flex items-center justify-between">
597→ <h2 className="font-semibold">{column.title}</h2>
598→ <span className="text-sm text-slate-400 bg-slate-700 px-2 py-0.5 rounded">
599→ {getTasksByStatus(column.id).length}
600→ </span>
601→ </div>
602→ </div>
603→
604→ {/* Column Content */}
605→ <div className="p-3 space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
606→ {isLoading && tasks.length === 0 ? (
607→ <div className="text-center py-8 text-slate-500 text-sm">
608→ Loading...
609→ </div>
610→ ) : getTasksByStatus(column.id).length === 0 ? (
611→ <div className="text-center py-8 text-slate-500 text-sm">
612→ No tasks
613→ </div>
614→ ) : (
615→ getTasksByStatus(column.id).map((task) => (
616→ <div
617→ key={task.id}
618→ draggable
619→ onDragStart={() => handleDragStart(task.id)}
620→ onClick={() => handleSelectTask(task)}
621→ className={`kanban-card bg-slate-900 rounded-lg p-4 border border-slate-700 cursor-pointer hover:border-slate-600 ${
622→ draggedTask === task.id ? 'opacity-50' : ''
623→ } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
624→ >
625→ <div className="flex items-start gap-2">
626→ <GripVertical size={16} className="text-slate-600 mt-0.5 flex-shrink-0" />
627→ <div className="flex-1 min-w-0">
628→ <h3 className="font-medium mb-1">{task.name}</h3>
629→ {task.description && (
630→ <p className="text-sm text-slate-400 line-clamp-2 mb-2">
631→ {task.description}
632→ </p>
633→ )}
634→ <div className="flex items-center justify-between">
635→ <span
636→ className={`badge ${
637→ column.id === 'backlog'
638→ ? 'badge-backlog'
639→ : column.id === 'in_progress'
640→ ? 'badge-in-progress'
641→ : column.id === 'qa'
642→ ? 'badge-qa'
643→ : column.id === 'done'
644→ ? 'badge-done'
645→ : 'badge-blocked'
646→ }`}
647→ >
648→ {column.title}
649→ </span>
650→ <span className={`text-xs ${
651→ task.priority === 3 ? 'text-destructive' :
652→ task.priority === 2 ? 'text-warning' :
653→ task.priority === 1 ? 'text-accent' :
654→ 'text-slate-500'
655→ }`}>
656→ {task.priority === 3 ? 'Critical' :
657→ task.priority === 2 ? 'High' :
658→ task.priority === 1 ? 'Medium' : 'Low'}
659→ </span>
660→ </div>
661→ </div>
662→ </div>
663→ </div>
664→ ))
665→ )}
666→ </div>
667→ </div>
668→ ))}
669→ </div>
670→ )}
671→
672→ {/* Task Details Panel */}
673→ {selectedTask && (
674→ <div className="w-96 flex-shrink-0 bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
675→ <div className="p-4 border-b border-slate-700 flex items-center justify-between">
676→ <h2 className="font-semibold text-lg">Task Details</h2>
677→ <div className="flex items-center gap-2">
678→ {!isEditing && (
679→ <>
680→ <button
681→ onClick={handleStartEdit}
682→ className="text-slate-400 hover:text-accent transition-colors"
683→ title="Edit Task"
684→ >
685→ <Pencil size={18} />
686→ </button>
687→ <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
688→ <AlertDialog.Trigger asChild>
689→ <button
690→ className="text-slate-400 hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
691→ title="Delete Task"
692→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
693→ >
694→ <Trash2 size={18} />
695→ </button>
696→ </AlertDialog.Trigger>
697→ <AlertDialog.Portal>
698→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
699→ <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
700→ <AlertDialog.Title className="text-xl font-semibold text-white">
701→ Delete Task
702→ </AlertDialog.Title>
703→ <AlertDialog.Description className="mt-3 text-slate-400">
704→ Are you sure you want to delete <span className="font-semibold text-white">{selectedTask.name}</span>?
705→ This action cannot be undone.
706→ </AlertDialog.Description>
707→ <div className="mt-6 flex justify-end gap-3">
708→ <AlertDialog.Cancel asChild>
709→ <button className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors">
710→ Cancel
711→ </button>
712→ </AlertDialog.Cancel>
713→ <AlertDialog.Action asChild>
714→ <button
715→ onClick={handleDeleteTask}
716→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
717→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
718→ >
719→ {isDeleting || (selectedTask && isTaskDeleting(selectedTask.id)) ? 'Deleting...' : 'Delete Task'}
720→ </button>
721→ </AlertDialog.Action>
722→ </div>
723→ </AlertDialog.Content>
724→ </AlertDialog.Portal>
725→ </AlertDialog.Root>
726→ </>
727→ )}
728→ <button
729→ onClick={handleClosePanel}
730→ className="text-slate-400 hover:text-white transition-colors"
731→ title="Close"
732→ >
733→ <X size={20} />
734→ </button>
735→ </div>
736→ </div>
737→ <div className="p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-300px)]">
738→ {isEditing ? (
739→ /* Edit Form */
740→ <div className="space-y-4">
741→ <div>
742→ <label className="block text-sm text-slate-400 mb-1">Name *</label>
743→ <input
744→ type="text"
745→ value={editName}
746→ onChange={(e) => setEditName(e.target.value)}
747→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
748→ placeholder="Task name"
749→ />
750→ </div>
751→ <div>
752→ <label className="block text-sm text-slate-400 mb-1">Description</label>
753→ <textarea
754→ value={editDescription}
755→ onChange={(e) => setEditDescription(e.target.value)}
756→ rows={3}
757→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
758→ placeholder="Task description"
759→ />
760→ </div>
761→ <div>
762→ <label className="block text-sm text-slate-400 mb-1">Priority</label>
763→ <select
764→ value={editPriority}
765→ onChange={(e) => setEditPriority(Number(e.target.value))}
766→ className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
767→ >
768→ <option value={0}>Low</option>
769→ <option value={1}>Medium</option>
770→ <option value={2}>High</option>
771→ <option value={3}>Critical</option>
772→ </select>
773→ </div>
774→ <div className="flex gap-2 pt-2">
775→ <button
776→ onClick={handleCancelEdit}
777→ className="flex-1 px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
778→ >
779→ Cancel
780→ </button>
781→ <button
782→ onClick={handleSaveEdit}
783→ disabled={isSaving || !editName.trim()}
784→ className="flex-1 flex items-center justify-center gap-2 px-3 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
785→ >
786→ {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
787→ </button>
788→ </div>
789→ </div>
790→ ) : (
791→ /* View Mode */
792→ <>
793→ {/* Task Name */}
794→ <div>
795→ <label className="block text-sm text-slate-400 mb-1">Name</label>
796→ <p className="font-medium text-lg">{selectedTask.name}</p>
797→ </div>
798→
799→ {/* Description */}
800→ <div>
801→ <label className="block text-sm text-slate-400 mb-1">Description</label>
802→ <p className="text-slate-300">
803→ {selectedTask.description || <span className="italic text-slate-500">No description</span>}
804→ </p>
805→ </div>
806→
807→ {/* Status */}
808→ <div>
809→ <label className="block text-sm text-slate-400 mb-1">Status</label>
810→ <span className={`badge ${
811→ selectedTask.status === 'backlog' ? 'badge-backlog' :
812→ selectedTask.status === 'in_progress' ? 'badge-in-progress' :
813→ selectedTask.status === 'qa' ? 'badge-qa' :
814→ selectedTask.status === 'done' ? 'badge-done' :
815→ 'badge-blocked'
816→ }`}>
817→ {STATUS_LABELS[selectedTask.status] || selectedTask.status}
818→ </span>
819→ </div>
820→
821→ {/* Priority */}
822→ <div>
823→ <label className="block text-sm text-slate-400 mb-1">Priority</label>
824→ <span className={`font-medium ${
825→ selectedTask.priority === 3 ? 'text-destructive' :
826→ selectedTask.priority === 2 ? 'text-warning' :
827→ selectedTask.priority === 1 ? 'text-accent' :
828→ 'text-slate-400'
829→ }`}>
830→ {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}
831→ </span>
832→ </div>
833→
834→ {/* Agent Assignment */}
835→ <div>
836→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
837→ <User size={14} />
838→ Agent Assignment
839→ </label>
840→ <p className="text-slate-300">
841→ {selectedTask.agent_type ? (
842→ <span className="capitalize">{selectedTask.agent_type.replace('_', ' ')}</span>
843→ ) : (
844→ <span className="italic text-slate-500">Not assigned</span>
845→ )}
846→ </p>
847→ </div>
848→
849→ {/* Iteration Count */}
850→ <div>
851→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
852→ <RefreshCw size={14} />
853→ Iteration Count
854→ </label>
855→ <p className="text-slate-300">{selectedTask.iteration_count}</p>
856→ </div>
857→
858→ {/* Flagged for Review */}
859→ <div>
860→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
861→ <Flag size={14} />
862→ Flagged for Review
863→ </label>
864→ <p className={selectedTask.flagged_for_review ? 'text-warning' : 'text-slate-300'}>
865→ {selectedTask.flagged_for_review ? 'Yes' : 'No'}
866→ </p>
867→ </div>
868→
869→ {/* Time Estimates */}
870→ {(selectedTask.estimated_time || selectedTask.actual_time) && (
871→ <div>
872→ <label className="block text-sm text-slate-400 mb-1 flex items-center gap-1">
873→ <Clock size={14} />
874→ Time
875→ </label>
876→ <div className="text-slate-300 text-sm">
877→ {selectedTask.estimated_time && <p>Estimated: {selectedTask.estimated_time} min</p>}
878→ {selectedTask.actual_time && <p>Actual: {selectedTask.actual_time} min</p>}
879→ </div>
880→ </div>
881→ )}
882→
883→ {/* Created/Updated timestamps */}
884→ <div className="pt-4 border-t border-slate-700 text-xs text-slate-500">
885→ <p>Created: {new Date(selectedTask.created_at).toLocaleString()}</p>
886→ <p>Updated: {new Date(selectedTask.updated_at).toLocaleString()}</p>
887→ </div>
888→ </>
889→ )}
890→ </div>
891→ </div>
892→ )}
893→
894→ {/* Unsaved Changes Warning Dialog */}
895→ <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
896→ <AlertDialog.Portal>
897→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
898→ <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-slate-800 border border-slate-700 rounded-lg p-6 w-full max-w-md z-50 shadow-xl">
899→ <AlertDialog.Title className="text-xl font-semibold text-white">
900→ Unsaved Changes
901→ </AlertDialog.Title>
902→ <AlertDialog.Description className="mt-3 text-slate-400">
903→ You have unsaved changes. Are you sure you want to discard them?
904→ </AlertDialog.Description>
905→ <div className="mt-6 flex justify-end gap-3">
906→ <AlertDialog.Cancel asChild>
907→ <button
908→ onClick={handleKeepEditing}
909→ className="px-4 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
910→ >
911→ Keep Editing
912→ </button>
913→ </AlertDialog.Cancel>
914→ <AlertDialog.Action asChild>
915→ <button
916→ onClick={handleDiscardChanges}
917→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
918→ >
919→ Discard Changes
920→ </button>
921→ </AlertDialog.Action>
922→ </div>
923→ </AlertDialog.Content>
924→ </AlertDialog.Portal>
925→ </AlertDialog.Root>
926→ </div>
927→ </div>
928→ )
929→}
930→
<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>