Files
claudetools/imported-conversations/auto-builder/415a52d7-00db-40bd-a68e-2d70c815213a/tool-results/toolu_01PodFUBnbRXoLP9iqm8pP5v.txt
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

1231 lines
66 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, Link2, Layers } from 'lucide-react'
4→import * as Dialog from '@radix-ui/react-dialog'
5→import * as AlertDialog from '@radix-ui/react-alert-dialog'
6→import * as Tooltip from '@radix-ui/react-tooltip'
7→import { useProjectStore } from '../stores/projectStore'
8→import { useTaskStore, Task, TaskCreate, TaskUpdate } from '../stores/taskStore'
9→import { usePhaseStore } from '../stores/phaseStore'
10→import { useToastStore } from '../components/Toast'
11→
12→const ITEMS_PER_PAGE = 10
13→
14→const COLUMNS = [
15→ { id: 'backlog', title: 'Backlog', color: 'border-slate-500' },
16→ { id: 'in_progress', title: 'In Progress', color: 'border-accent' },
17→ { id: 'qa', title: 'QA', color: 'border-primary-400' },
18→ { id: 'done', title: 'Done', color: 'border-green-400' },
19→ { id: 'blocked', title: 'Blocked', color: 'border-destructive' },
20→]
21→
22→const PRIORITY_LABELS: Record<number, string> = {
23→ 0: 'Low',
24→ 1: 'Medium',
25→ 2: 'High',
26→ 3: 'Critical',
27→}
28→
29→const STATUS_LABELS: Record<string, string> = {
30→ backlog: 'Backlog',
31→ in_progress: 'In Progress',
32→ qa: 'QA',
33→ done: 'Done',
34→ blocked: 'Blocked',
35→}
36→
37→export default function KanbanBoard() {
38→ const [searchParams, setSearchParams] = useSearchParams()
39→ const { currentProject } = useProjectStore()
40→ const { tasks, fetchTasks, createTask, updateTask, updateTaskStatus, deleteTask, isTaskDeleting, isLoading } = useTaskStore()
41→ const { phases, fetchPhases } = usePhaseStore()
42→ const { addToast } = useToastStore()
43→ const [showAddDialog, setShowAddDialog] = useState(false)
44→ const [newTaskName, setNewTaskName] = useState('')
45→ const [newTaskDescription, setNewTaskDescription] = useState('')
46→ const [newTaskPriority, setNewTaskPriority] = useState(0)
47→ const [newTaskPhaseId, setNewTaskPhaseId] = useState<number | null>(null)
48→ const [isCreating, setIsCreating] = useState(false)
49→ const [draggedTask, setDraggedTask] = useState<number | null>(null)
50→ const [selectedTask, setSelectedTask] = useState<Task | null>(null)
51→ const [isEditing, setIsEditing] = useState(false)
52→ const [editName, setEditName] = useState('')
53→ const [editDescription, setEditDescription] = useState('')
54→ const [editPriority, setEditPriority] = useState(0)
55→ const [editPhaseId, setEditPhaseId] = useState<number | null>(null)
56→ const [isSaving, setIsSaving] = useState(false)
57→ const [showDeleteDialog, setShowDeleteDialog] = useState(false)
58→ const [isDeleting, setIsDeleting] = useState(false)
59→ const [showUnsavedDialog, setShowUnsavedDialog] = useState(false)
60→ const [pendingCloseAction, setPendingCloseAction] = useState<'close' | 'cancel' | null>(null)
61→ // Filter and Sort state - initialize from URL params
62→ const getInitialPriorityFilter = (): number | 'all' => {
63→ const param = searchParams.get('priority')
64→ if (param === 'all' || param === null) return 'all'
65→ const num = parseInt(param, 10)
66→ return [0, 1, 2, 3].includes(num) ? num : 'all'
67→ }
68→ const getInitialStatusFilter = (): string | 'all' => {
69→ const param = searchParams.get('status')
70→ if (param === 'all' || param === null) return 'all'
71→ const validStatuses = ['backlog', 'in_progress', 'qa', 'done', 'blocked']
72→ return validStatuses.includes(param) ? param : 'all'
73→ }
74→ const getInitialSortOrder = (): 'newest' | 'oldest' | 'priority' => {
75→ const param = searchParams.get('sort')
76→ if (param === 'newest' || param === 'oldest' || param === 'priority') return param
77→ return 'newest'
78→ }
79→ const getInitialAgentTypeFilter = (): string | 'all' => {
80→ const param = searchParams.get('agent')
81→ if (param === 'all' || param === null) return 'all'
82→ return param
83→ }
84→ const getInitialViewMode = (): 'kanban' | 'list' => {
85→ const param = searchParams.get('view')
86→ if (param === 'list') return 'list'
87→ return 'kanban'
88→ }
89→
90→ const [priorityFilter, setPriorityFilter] = useState<number | 'all'>(getInitialPriorityFilter)
91→ const [statusFilter, setStatusFilter] = useState<string | 'all'>(getInitialStatusFilter)
92→ const [sortOrder, setSortOrder] = useState<'newest' | 'oldest' | 'priority'>(getInitialSortOrder)
93→ const [agentTypeFilter, setAgentTypeFilter] = useState<string | 'all'>(getInitialAgentTypeFilter)
94→ // View mode and pagination state
95→ const [viewMode, setViewMode] = useState<'kanban' | 'list'>(getInitialViewMode)
96→ // Phase grouping state
97→ const [phaseGrouping, setPhaseGrouping] = useState<boolean>(() => searchParams.get('groupByPhase') === 'true')
98→ const [currentPage, setCurrentPage] = useState(1)
99→
100→ useEffect(() => {
101→ if (currentProject?.id) {
102→ fetchTasks(currentProject.id)
103→ fetchPhases(currentProject.id)
104→ }
105→ }, [currentProject?.id, fetchTasks, fetchPhases])
106→
107→ // Handle deep linking - select task from URL param after tasks are loaded
108→ useEffect(() => {
109→ const taskIdParam = searchParams.get('task')
110→ if (taskIdParam && tasks.length > 0) {
111→ const taskId = parseInt(taskIdParam, 10)
112→ const task = tasks.find(t => t.id === taskId)
113→ if (task) {
114→ setSelectedTask(task)
115→ }
116→ }
117→ }, [searchParams, tasks])
118→
119→ // Update URL when task is selected
120→ const handleSelectTask = (task: Task) => {
121→ setSelectedTask(task)
122→ const newParams = new URLSearchParams(searchParams)
123→ newParams.set('task', task.id.toString())
124→ setSearchParams(newParams)
125→ }
126→
127→ // Clear task from URL when panel is closed
128→ const clearTaskFromUrl = () => {
129→ const newParams = new URLSearchParams(searchParams)
130→ newParams.delete('task')
131→ setSearchParams(newParams)
132→ }
133→
134→ // Update URL when filter changes
135→ const handlePriorityFilterChange = (value: number | 'all') => {
136→ setPriorityFilter(value)
137→ const newParams = new URLSearchParams(searchParams)
138→ if (value === 'all') {
139→ newParams.delete('priority')
140→ } else {
141→ newParams.set('priority', value.toString())
142→ }
143→ setSearchParams(newParams)
144→ }
145→
146→ // Update URL when status filter changes
147→ const handleStatusFilterChange = (value: string | 'all') => {
148→ setStatusFilter(value)
149→ const newParams = new URLSearchParams(searchParams)
150→ if (value === 'all') {
151→ newParams.delete('status')
152→ } else {
153→ newParams.set('status', value)
154→ }
155→ setSearchParams(newParams)
156→ }
157→
158→ // Update URL when sort changes
159→ const handleSortOrderChange = (value: 'newest' | 'oldest' | 'priority') => {
160→ setSortOrder(value)
161→ const newParams = new URLSearchParams(searchParams)
162→ if (value === 'newest') {
163→ newParams.delete('sort') // default value, no need to store
164→ } else {
165→ newParams.set('sort', value)
166→ }
167→ setSearchParams(newParams)
168→ }
169→
170→ // Update URL when agent type filter changes
171→ const handleAgentTypeFilterChange = (value: string | 'all') => {
172→ setAgentTypeFilter(value)
173→ const newParams = new URLSearchParams(searchParams)
174→ if (value === 'all') {
175→ newParams.delete('agent')
176→ } else {
177→ newParams.set('agent', value)
178→ }
179→ setSearchParams(newParams)
180→ }
181→
182→ // Update URL when view mode changes
183→ const handleViewModeChange = (value: 'kanban' | 'list') => {
184→ setViewMode(value)
185→ const newParams = new URLSearchParams(searchParams)
186→ if (value === 'kanban') {
187→ newParams.delete('view') // default value, no need to store
188→ } else {
189→ newParams.set('view', value)
190→ }
191→ setSearchParams(newParams)
192→ }
193→
194→ // Reset all filters to defaults
195→ const handleResetFilters = () => {
196→ setPriorityFilter('all')
197→ setStatusFilter('all')
198→ setAgentTypeFilter('all')
199→ setSortOrder('newest')
200→ setCurrentPage(1)
201→ const newParams = new URLSearchParams(searchParams)
202→ newParams.delete('priority')
203→ newParams.delete('status')
204→ newParams.delete('agent')
205→ newParams.delete('sort')
206→ setSearchParams(newParams)
207→ }
208→
209→ // Check if filters are active (non-default)
210→ const hasActiveFilters = priorityFilter !== 'all' || statusFilter !== 'all' || agentTypeFilter !== 'all' || sortOrder !== 'newest'
211→
212→ // Filtered and sorted tasks
213→ const filteredAndSortedTasks = useMemo(() => {
214→ let result = [...tasks]
215→
216→ // Apply priority filter
217→ if (priorityFilter !== 'all') {
218→ result = result.filter(task => task.priority === priorityFilter)
219→ }
220→
221→ // Apply status filter
222→ if (statusFilter !== 'all') {
223→ result = result.filter(task => task.status === statusFilter)
224→ }
225→
226→ // Apply agent type filter
227→ if (agentTypeFilter !== 'all') {
228→ result = result.filter(task => task.agent_type === agentTypeFilter)
229→ }
230→
231→ // Apply sort
232→ result.sort((a, b) => {
233→ if (sortOrder === 'newest') {
234→ return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
235→ } else if (sortOrder === 'oldest') {
236→ return new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
237→ } else {
238→ // priority - highest first
239→ return b.priority - a.priority
240→ }
241→ })
242→
243→ return result
244→ }, [tasks, priorityFilter, statusFilter, agentTypeFilter, sortOrder])
245→
246→ // Pagination calculations
247→ const totalPages = Math.ceil(filteredAndSortedTasks.length / ITEMS_PER_PAGE)
248→ const paginatedTasks = useMemo(() => {
249→ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE
250→ return filteredAndSortedTasks.slice(startIndex, startIndex + ITEMS_PER_PAGE)
251→ }, [filteredAndSortedTasks, currentPage])
252→
253→ // Reset page when filter changes
254→ useEffect(() => {
255→ setCurrentPage(1)
256→ }, [priorityFilter, statusFilter, agentTypeFilter, sortOrder])
257→
258→ const getTasksByStatus = (status: string) => {
259→ return filteredAndSortedTasks.filter((task) => task.status === status)
260→ }
261→
262→ // Helper to get dependency task names
263→ const getDependencyNames = (dependsOn: number[] | null): string[] => {
264→ if (!dependsOn || dependsOn.length === 0) return []
265→ return dependsOn
266→ .map(taskId => tasks.find(t => t.id === taskId)?.name || `Task #${taskId}`)
267→ }
268→
269→ const handleCreateTask = async () => {
270→ if (!currentProject || !newTaskName.trim()) return
271→ setIsCreating(true)
272→ try {
273→ const taskData: TaskCreate = {
274→ name: newTaskName.trim(),
275→ description: newTaskDescription.trim() || undefined,
276→ priority: newTaskPriority,
277→ phase_id: newTaskPhaseId,
278→ }
279→ await createTask(currentProject.id, taskData)
280→ setNewTaskName('')
281→ setNewTaskDescription('')
282→ setNewTaskPriority(0)
283→ setNewTaskPhaseId(null)
284→ setShowAddDialog(false)
285→ } catch (error) {
286→ console.error('Failed to create task:', error)
287→ addToast('error', 'Create Failed', 'Failed to create task. Please try again.')
288→ } finally {
289→ setIsCreating(false)
290→ }
291→ }
292→
293→ const handleDragStart = (taskId: number) => {
294→ setDraggedTask(taskId)
295→ }
296→
297→ const handleDragOver = (e: React.DragEvent) => {
298→ e.preventDefault()
299→ }
300→
301→ const handleDrop = async (targetStatus: string) => {
302→ if (draggedTask === null) return
303→ try {
304→ await updateTaskStatus(draggedTask, targetStatus)
305→ } catch (error) {
306→ console.error('Failed to update task status:', error)
307→ addToast('error', 'Update Failed', 'Failed to update task status. Please try again.')
308→ }
309→ setDraggedTask(null)
310→ }
311→
312→ const handleStartEdit = () => {
313→ if (!selectedTask) return
314→ setEditName(selectedTask.name)
315→ setEditDescription(selectedTask.description || '')
316→ setEditPriority(selectedTask.priority)
317→ setEditPhaseId(selectedTask.phase_id)
318→ setIsEditing(true)
319→ }
320→
321→ // Check if edit form has unsaved changes
322→ const hasUnsavedChanges = () => {
323→ if (!selectedTask || !isEditing) return false
324→ return (
325→ editName.trim() !== selectedTask.name ||
326→ (editDescription.trim() || '') !== (selectedTask.description || '') ||
327→ editPriority !== selectedTask.priority ||
328→ editPhaseId !== selectedTask.phase_id
329→ )
330→ }
331→
332→ const handleCancelEdit = () => {
333→ if (hasUnsavedChanges()) {
334→ setPendingCloseAction('cancel')
335→ setShowUnsavedDialog(true)
336→ } else {
337→ setIsEditing(false)
338→ }
339→ }
340→
341→ const handleClosePanel = () => {
342→ if (hasUnsavedChanges()) {
343→ setPendingCloseAction('close')
344→ setShowUnsavedDialog(true)
345→ } else {
346→ setSelectedTask(null)
347→ setIsEditing(false)
348→ clearTaskFromUrl()
349→ }
350→ }
351→
352→ const handleDiscardChanges = () => {
353→ setShowUnsavedDialog(false)
354→ if (pendingCloseAction === 'close') {
355→ setSelectedTask(null)
356→ clearTaskFromUrl()
357→ }
358→ setIsEditing(false)
359→ setPendingCloseAction(null)
360→ }
361→
362→ const handleKeepEditing = () => {
363→ setShowUnsavedDialog(false)
364→ setPendingCloseAction(null)
365→ }
366→
367→ const handleSaveEdit = async () => {
368→ if (!selectedTask) return
369→ setIsSaving(true)
370→ try {
371→ const updates: TaskUpdate = {
372→ name: editName.trim(),
373→ description: editDescription.trim() || undefined,
374→ priority: editPriority,
375→ phase_id: editPhaseId,
376→ }
377→ const updatedTask = await updateTask(selectedTask.id, updates)
378→ setSelectedTask(updatedTask)
379→ setIsEditing(false)
380→ } catch (error) {
381→ console.error('Failed to update task:', error)
382→ addToast('error', 'Save Failed', 'Failed to save task changes. Please try again.')
383→ } finally {
384→ setIsSaving(false)
385→ }
386→ }
387→
388→ const handleDeleteTask = async () => {
389→ if (!selectedTask) return
390→ // Check if already being deleted (prevents rapid clicks)
391→ if (isTaskDeleting(selectedTask.id)) return
392→
393→ const taskIdToDelete = selectedTask.id
394→ const taskName = selectedTask.name
395→ setIsDeleting(true)
396→ // Close dialog immediately to prevent additional clicks
397→ setShowDeleteDialog(false)
398→
399→ try {
400→ await deleteTask(taskIdToDelete)
401→ setSelectedTask(null)
402→ clearTaskFromUrl()
403→ } catch (error) {
404→ console.error('Failed to delete task:', error)
405→ addToast('error', 'Delete Failed', `Failed to delete task "${taskName}". Please try again.`)
406→ // Re-open dialog if delete failed so user can retry
407→ setShowDeleteDialog(true)
408→ } finally {
409→ setIsDeleting(false)
410→ }
411→ }
412→
413→ if (!currentProject) {
414→ return (
415→ <div className="flex items-center justify-center h-full">
416→ <p className="text-muted-foreground">Select a project to view its Kanban board</p>
417→ </div>
418→ )
419→ }
420→
421→ return (
422→ <div className="h-full flex flex-col">
423→ {/* Header */}
424→ <div className="flex items-center justify-between mb-6">
425→ <h1 className="text-2xl font-bold">Kanban Board</h1>
426→ <div className="flex items-center gap-4">
427→ {/* Priority Filter */}
428→ <div className="flex items-center gap-2">
429→ <Filter size={16} className="text-muted-foreground" />
430→ <select
431→ value={priorityFilter === 'all' ? 'all' : priorityFilter.toString()}
432→ onChange={(e) => handlePriorityFilterChange(e.target.value === 'all' ? 'all' : Number(e.target.value))}
433→ className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
434→ >
435→ <option value="all">All Priorities</option>
436→ <option value="3">Critical</option>
437→ <option value="2">High</option>
438→ <option value="1">Medium</option>
439→ <option value="0">Low</option>
440→ </select>
441→ </div>
442→
443→ {/* Status Filter */}
444→ <div className="flex items-center gap-2">
445→ <select
446→ value={statusFilter}
447→ onChange={(e) => handleStatusFilterChange(e.target.value)}
448→ className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
449→ >
450→ <option value="all">All Statuses</option>
451→ <option value="backlog">Backlog</option>
452→ <option value="in_progress">In Progress</option>
453→ <option value="qa">QA</option>
454→ <option value="done">Done</option>
455→ <option value="blocked">Blocked</option>
456→ </select>
457→ </div>
458→
459→ {/* Agent Type Filter */}
460→ <div className="flex items-center gap-2">
461→ <User size={16} className="text-muted-foreground" />
462→ <select
463→ value={agentTypeFilter}
464→ onChange={(e) => handleAgentTypeFilterChange(e.target.value)}
465→ className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
466→ >
467→ <option value="all">All Agents</option>
468→ <option value="coder">Coder Agent</option>
469→ <option value="qa_reviewer">QA Reviewer Agent</option>
470→ <option value="architect">Architect Agent</option>
471→ <option value="debugger">Debugger Agent</option>
472→ </select>
473→ </div>
474→
475→ {/* Sort Order */}
476→ <div className="flex items-center gap-2">
477→ <ArrowUpDown size={16} className="text-muted-foreground" />
478→ <select
479→ value={sortOrder}
480→ onChange={(e) => handleSortOrderChange(e.target.value as 'newest' | 'oldest' | 'priority')}
481→ className="px-3 py-1.5 bg-card border border-border rounded-lg text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent"
482→ >
483→ <option value="newest">Newest First</option>
484→ <option value="oldest">Oldest First</option>
485→ <option value="priority">By Priority</option>
486→ </select>
487→ </div>
488→
489→ {/* Reset Filters Button */}
490→ {hasActiveFilters && (
491→ <button
492→ onClick={handleResetFilters}
493→ className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-secondary rounded-lg transition-colors"
494→ title="Reset Filters"
495→ >
496→ <RefreshCw size={14} />
497→ Reset
498→ </button>
499→ )}
500→
501→ {/* View Mode Toggle */}
502→ <div className="flex items-center gap-1 bg-card border border-border rounded-lg p-1">
503→ <button
504→ onClick={() => handleViewModeChange('kanban')}
505→ className={`p-1.5 rounded ${viewMode === 'kanban' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
506→ title="Kanban View"
507→ >
508→ <LayoutGrid size={18} />
509→ </button>
510→ <button
511→ onClick={() => handleViewModeChange('list')}
512→ className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-accent text-white' : 'text-muted-foreground hover:text-foreground'}`}
513→ title="List View"
514→ >
515→ <List size={18} />
516→ </button>
517→ </div>
518→
519→ <Dialog.Root open={showAddDialog} onOpenChange={setShowAddDialog}>
520→ <Dialog.Trigger asChild>
521→ <button className="btn-accent flex items-center gap-2">
522→ <Plus size={18} />
523→ Add Task
524→ </button>
525→ </Dialog.Trigger>
526→ <Dialog.Portal>
527→ <Dialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
528→ <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
529→ <div className="flex items-center justify-between mb-4">
530→ <Dialog.Title className="text-xl font-semibold text-foreground">
531→ Add New Task
532→ </Dialog.Title>
533→ <Dialog.Close asChild>
534→ <button className="text-muted-foreground hover:text-foreground" aria-label="Close dialog">
535→ <X size={20} />
536→ </button>
537→ </Dialog.Close>
538→ </div>
539→ <Dialog.Description className="text-muted-foreground mb-4">
540→ Create a new task for this project.
541→ </Dialog.Description>
542→ <div className="space-y-4">
543→ <div>
544→ <label className="block text-sm font-medium text-secondary-foreground mb-1">
545→ Task Name *
546→ </label>
547→ <input
548→ type="text"
549→ value={newTaskName}
550→ onChange={(e) => setNewTaskName(e.target.value)}
551→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
552→ placeholder="Enter task name"
553→ />
554→ </div>
555→ <div>
556→ <label className="block text-sm font-medium text-secondary-foreground mb-1">
557→ Description
558→ </label>
559→ <textarea
560→ value={newTaskDescription}
561→ onChange={(e) => setNewTaskDescription(e.target.value)}
562→ rows={3}
563→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
564→ placeholder="Enter task description"
565→ />
566→ </div>
567→ <div>
568→ <label className="block text-sm font-medium text-secondary-foreground mb-1">
569→ Priority
570→ </label>
571→ <select
572→ value={newTaskPriority}
573→ onChange={(e) => setNewTaskPriority(Number(e.target.value))}
574→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
575→ >
576→ <option value={0}>Low</option>
577→ <option value={1}>Medium</option>
578→ <option value={2}>High</option>
579→ <option value={3}>Critical</option>
580→ </select>
581→ </div>
582→ <div>
583→ <label className="block text-sm font-medium text-secondary-foreground mb-1">
584→ Phase
585→ </label>
586→ <select
587→ value={newTaskPhaseId ?? ''}
588→ onChange={(e) => setNewTaskPhaseId(e.target.value ? Number(e.target.value) : null)}
589→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
590→ >
591→ <option value="">No Phase</option>
592→ {phases.map((phase) => (
593→ <option key={phase.id} value={phase.id}>{phase.name}</option>
594→ ))}
595→ </select>
596→ </div>
597→ </div>
598→ <div className="mt-6 flex justify-end gap-3">
599→ <Dialog.Close asChild>
600→ <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
601→ Cancel
602→ </button>
603→ </Dialog.Close>
604→ <button
605→ onClick={handleCreateTask}
606→ disabled={isCreating || !newTaskName.trim()}
607→ className="px-4 py-2 text-sm bg-accent hover:bg-accent/80 text-white rounded-lg transition-colors disabled:opacity-50"
608→ >
609→ {isCreating ? 'Creating...' : 'Create Task'}
610→ </button>
611→ </div>
612→ </Dialog.Content>
613→ </Dialog.Portal>
614→ </Dialog.Root>
615→ </div>
616→ </div>
617→
618→ {/* Main Content Area */}
619→ <div className="flex-1 flex gap-4 overflow-hidden">
620→ {viewMode === 'list' ? (
621→ /* List View with Pagination */
622→ <div className="flex-1 flex flex-col">
623→ {/* Task List Table */}
624→ <div className="flex-1 overflow-auto bg-card rounded-lg border border-border">
625→ <table className="w-full">
626→ <thead className="bg-background sticky top-0">
627→ <tr className="text-left text-sm text-muted-foreground">
628→ <th className="px-4 py-3 font-medium">Task Name</th>
629→ <th className="px-4 py-3 font-medium">Status</th>
630→ <th className="px-4 py-3 font-medium">Priority</th>
631→ <th className="px-4 py-3 font-medium">Created</th>
632→ </tr>
633→ </thead>
634→ <tbody>
635→ {isLoading && tasks.length === 0 ? (
636→ <tr>
637→ <td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">
638→ Loading...
639→ </td>
640→ </tr>
641→ ) : paginatedTasks.length === 0 ? (
642→ <tr>
643→ <td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">
644→ No tasks found
645→ </td>
646→ </tr>
647→ ) : (
648→ paginatedTasks.map((task) => (
649→ <tr
650→ key={task.id}
651→ onClick={() => handleSelectTask(task)}
652→ className={`border-t border-border hover:bg-secondary/50 cursor-pointer ${
653→ selectedTask?.id === task.id ? 'bg-secondary' : ''
654→ }`}
655→ >
656→ <td className="px-4 py-3">
657→ <div className="flex items-center gap-2">
658→ <div className="font-medium">{task.name}</div>
659→ {/* Flagged for Review Indicator in List View */}
660→ {task.flagged_for_review && (
661→ <Tooltip.Provider>
662→ <Tooltip.Root>
663→ <Tooltip.Trigger asChild>
664→ <span className="flex items-center text-warning cursor-help">
665→ <Flag size={12} />
666→ </span>
667→ </Tooltip.Trigger>
668→ <Tooltip.Portal>
669→ <Tooltip.Content
670→ className="bg-popover text-popover-foreground px-3 py-2 rounded-lg shadow-lg border border-border text-sm max-w-xs z-50"
671→ sideOffset={5}
672→ >
673→ Flagged for human review
674→ <Tooltip.Arrow className="fill-popover" />
675→ </Tooltip.Content>
676→ </Tooltip.Portal>
677→ </Tooltip.Root>
678→ </Tooltip.Provider>
679→ )}
680→ {/* Dependency Indicator in List View */}
681→ {task.depends_on && task.depends_on.length > 0 && (
682→ <Tooltip.Provider>
683→ <Tooltip.Root>
684→ <Tooltip.Trigger asChild>
685→ <span className="flex items-center gap-1 text-xs text-primary-400 cursor-help">
686→ <Link2 size={12} />
687→ <span>{task.depends_on.length}</span>
688→ </span>
689→ </Tooltip.Trigger>
690→ <Tooltip.Portal>
691→ <Tooltip.Content
692→ className="bg-popover text-popover-foreground px-3 py-2 rounded-lg shadow-lg border border-border text-sm max-w-xs z-50"
693→ sideOffset={5}
694→ >
695→ <div className="font-medium mb-1">Depends on:</div>
696→ <ul className="list-disc list-inside text-muted-foreground">
697→ {getDependencyNames(task.depends_on).map((name, idx) => (
698→ <li key={idx} className="truncate">{name}</li>
699→ ))}
700→ </ul>
701→ <Tooltip.Arrow className="fill-popover" />
702→ </Tooltip.Content>
703→ </Tooltip.Portal>
704→ </Tooltip.Root>
705→ </Tooltip.Provider>
706→ )}
707→ </div>
708→ {task.description && (
709→ <div className="text-sm text-muted-foreground truncate max-w-md">
710→ {task.description}
711→ </div>
712→ )}
713→ </td>
714→ <td className="px-4 py-3">
715→ <span className={`badge ${
716→ task.status === 'backlog' ? 'badge-backlog' :
717→ task.status === 'in_progress' ? 'badge-in-progress' :
718→ task.status === 'qa' ? 'badge-qa' :
719→ task.status === 'done' ? 'badge-done' :
720→ 'badge-blocked'
721→ }`}>
722→ {STATUS_LABELS[task.status] || task.status}
723→ </span>
724→ </td>
725→ <td className="px-4 py-3">
726→ <span className={`text-sm ${
727→ task.priority === 3 ? 'text-destructive' :
728→ task.priority === 2 ? 'text-warning' :
729→ task.priority === 1 ? 'text-accent' :
730→ 'text-muted-foreground'
731→ }`}>
732→ {PRIORITY_LABELS[task.priority] || 'Unknown'}
733→ </span>
734→ </td>
735→ <td className="px-4 py-3 text-sm text-muted-foreground">
736→ {new Date(task.created_at).toLocaleDateString()}
737→ </td>
738→ </tr>
739→ ))
740→ )}
741→ </tbody>
742→ </table>
743→ </div>
744→
745→ {/* Pagination Controls */}
746→ {totalPages > 1 && (
747→ <div className="flex items-center justify-between mt-4 px-2">
748→ <div className="text-sm text-muted-foreground">
749→ Showing {((currentPage - 1) * ITEMS_PER_PAGE) + 1} to {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedTasks.length)} of {filteredAndSortedTasks.length} tasks
750→ </div>
751→ <div className="flex items-center gap-2">
752→ <button
753→ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
754→ disabled={currentPage === 1}
755→ className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
756→ >
757→ <ChevronLeft size={18} />
758→ </button>
759→ <div className="flex items-center gap-1">
760→ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
761→ <button
762→ key={page}
763→ onClick={() => setCurrentPage(page)}
764→ className={`w-8 h-8 rounded-lg text-sm ${
765→ page === currentPage
766→ ? 'bg-accent text-white'
767→ : 'bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary'
768→ }`}
769→ >
770→ {page}
771→ </button>
772→ ))}
773→ </div>
774→ <button
775→ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
776→ disabled={currentPage === totalPages}
777→ className="p-2 rounded-lg bg-card border border-border text-muted-foreground hover:text-foreground hover:bg-secondary disabled:opacity-50 disabled:cursor-not-allowed"
778→ >
779→ <ChevronRight size={18} />
780→ </button>
781→ </div>
782→ </div>
783→ )}
784→ </div>
785→ ) : (
786→ /* Kanban Columns */
787→ <div className={`flex-1 flex gap-4 overflow-x-auto pb-4 ${selectedTask ? 'mr-0' : ''}`}>
788→ {COLUMNS.map((column) => (
789→ <div
790→ key={column.id}
791→ className={`flex-shrink-0 w-72 bg-card rounded-lg border-t-2 ${column.color}`}
792→ onDragOver={handleDragOver}
793→ onDrop={() => handleDrop(column.id)}
794→ >
795→ {/* Column Header */}
796→ <div className="p-4 border-b border-border">
797→ <div className="flex items-center justify-between">
798→ <h2 className="font-semibold">{column.title}</h2>
799→ <span className="text-sm text-muted-foreground bg-secondary px-2 py-0.5 rounded">
800→ {getTasksByStatus(column.id).length}
801→ </span>
802→ </div>
803→ </div>
804→
805→ {/* Column Content */}
806→ <div className="p-3 space-y-3 max-h-[calc(100vh-300px)] overflow-y-auto">
807→ {isLoading && tasks.length === 0 ? (
808→ <div className="text-center py-8 text-muted-foreground text-sm">
809→ Loading...
810→ </div>
811→ ) : getTasksByStatus(column.id).length === 0 ? (
812→ <div className="text-center py-8 text-muted-foreground text-sm">
813→ No tasks
814→ </div>
815→ ) : (
816→ getTasksByStatus(column.id).map((task) => (
817→ <div
818→ key={task.id}
819→ draggable
820→ onDragStart={() => handleDragStart(task.id)}
821→ onClick={() => handleSelectTask(task)}
822→ className={`kanban-card bg-background rounded-lg p-4 border border-border cursor-pointer hover:border-muted-foreground ${
823→ draggedTask === task.id ? 'opacity-50' : ''
824→ } ${selectedTask?.id === task.id ? 'ring-2 ring-accent' : ''}`}
825→ >
826→ <div className="flex items-start gap-2">
827→ <GripVertical size={16} className="text-muted-foreground mt-0.5 flex-shrink-0" />
828→ <div className="flex-1 min-w-0">
829→ <h3 className="font-medium mb-1">{task.name}</h3>
830→ {task.description && (
831→ <p className="text-sm text-muted-foreground line-clamp-2 mb-2">
832→ {task.description}
833→ </p>
834→ )}
835→ <div className="flex items-center justify-between">
836→ <div className="flex items-center gap-2">
837→ <span
838→ className={`badge ${
839→ column.id === 'backlog'
840→ ? 'badge-backlog'
841→ : column.id === 'in_progress'
842→ ? 'badge-in-progress'
843→ : column.id === 'qa'
844→ ? 'badge-qa'
845→ : column.id === 'done'
846→ ? 'badge-done'
847→ : 'badge-blocked'
848→ }`}
849→ >
850→ {column.title}
851→ </span>
852→ {/* Flagged for Review Indicator */}
853→ {task.flagged_for_review && (
854→ <Tooltip.Provider>
855→ <Tooltip.Root>
856→ <Tooltip.Trigger asChild>
857→ <span className="flex items-center text-warning cursor-help">
858→ <Flag size={12} />
859→ </span>
860→ </Tooltip.Trigger>
861→ <Tooltip.Portal>
862→ <Tooltip.Content
863→ className="bg-popover text-popover-foreground px-3 py-2 rounded-lg shadow-lg border border-border text-sm max-w-xs z-50"
864→ sideOffset={5}
865→ >
866→ Flagged for human review
867→ <Tooltip.Arrow className="fill-popover" />
868→ </Tooltip.Content>
869→ </Tooltip.Portal>
870→ </Tooltip.Root>
871→ </Tooltip.Provider>
872→ )}
873→ {/* Dependency Indicator */}
874→ {task.depends_on && task.depends_on.length > 0 && (
875→ <Tooltip.Provider>
876→ <Tooltip.Root>
877→ <Tooltip.Trigger asChild>
878→ <span className="flex items-center gap-1 text-xs text-primary-400 cursor-help">
879→ <Link2 size={12} />
880→ <span>{task.depends_on.length}</span>
881→ </span>
882→ </Tooltip.Trigger>
883→ <Tooltip.Portal>
884→ <Tooltip.Content
885→ className="bg-popover text-popover-foreground px-3 py-2 rounded-lg shadow-lg border border-border text-sm max-w-xs z-50"
886→ sideOffset={5}
887→ >
888→ <div className="font-medium mb-1">Depends on:</div>
889→ <ul className="list-disc list-inside text-muted-foreground">
890→ {getDependencyNames(task.depends_on).map((name, idx) => (
891→ <li key={idx} className="truncate">{name}</li>
892→ ))}
893→ </ul>
894→ <Tooltip.Arrow className="fill-popover" />
895→ </Tooltip.Content>
896→ </Tooltip.Portal>
897→ </Tooltip.Root>
898→ </Tooltip.Provider>
899→ )}
900→ </div>
901→ <span className={`text-xs ${
902→ task.priority === 3 ? 'text-destructive' :
903→ task.priority === 2 ? 'text-warning' :
904→ task.priority === 1 ? 'text-accent' :
905→ 'text-muted-foreground'
906→ }`}>
907→ {task.priority === 3 ? 'Critical' :
908→ task.priority === 2 ? 'High' :
909→ task.priority === 1 ? 'Medium' : 'Low'}
910→ </span>
911→ </div>
912→ </div>
913→ </div>
914→ </div>
915→ ))
916→ )}
917→ </div>
918→ </div>
919→ ))}
920→ </div>
921→ )}
922→
923→ {/* Task Details Panel */}
924→ {selectedTask && (
925→ <div className="w-96 flex-shrink-0 bg-card rounded-lg border border-border overflow-hidden">
926→ <div className="p-4 border-b border-border flex items-center justify-between">
927→ <h2 className="font-semibold text-lg">Task Details</h2>
928→ <div className="flex items-center gap-2">
929→ {!isEditing && (
930→ <>
931→ <button
932→ onClick={handleStartEdit}
933→ className="text-muted-foreground hover:text-accent transition-colors"
934→ title="Edit Task"
935→ >
936→ <Pencil size={18} />
937→ </button>
938→ <AlertDialog.Root open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
939→ <AlertDialog.Trigger asChild>
940→ <button
941→ className="text-muted-foreground hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
942→ title="Delete Task"
943→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
944→ >
945→ <Trash2 size={18} />
946→ </button>
947→ </AlertDialog.Trigger>
948→ <AlertDialog.Portal>
949→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
950→ <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
951→ <AlertDialog.Title className="text-xl font-semibold text-foreground">
952→ Delete Task
953→ </AlertDialog.Title>
954→ <AlertDialog.Description className="mt-3 text-muted-foreground">
955→ Are you sure you want to delete <span className="font-semibold text-foreground">{selectedTask.name}</span>?
956→ This action cannot be undone.
957→ </AlertDialog.Description>
958→ <div className="mt-6 flex justify-end gap-3">
959→ <AlertDialog.Cancel asChild>
960→ <button className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors">
961→ Cancel
962→ </button>
963→ </AlertDialog.Cancel>
964→ <AlertDialog.Action asChild>
965→ <button
966→ onClick={handleDeleteTask}
967→ disabled={isDeleting || (selectedTask ? isTaskDeleting(selectedTask.id) : false)}
968→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors disabled:opacity-50"
969→ >
970→ {isDeleting || (selectedTask && isTaskDeleting(selectedTask.id)) ? 'Deleting...' : 'Delete Task'}
971→ </button>
972→ </AlertDialog.Action>
973→ </div>
974→ </AlertDialog.Content>
975→ </AlertDialog.Portal>
976→ </AlertDialog.Root>
977→ </>
978→ )}
979→ <button
980→ onClick={handleClosePanel}
981→ className="text-muted-foreground hover:text-foreground transition-colors"
982→ title="Close"
983→ >
984→ <X size={20} />
985→ </button>
986→ </div>
987→ </div>
988→ <div className="p-4 space-y-4 overflow-y-auto max-h-[calc(100vh-300px)]">
989→ {isEditing ? (
990→ /* Edit Form */
991→ <div className="space-y-4">
992→ <div>
993→ <label className="block text-sm text-muted-foreground mb-1">Name *</label>
994→ <input
995→ type="text"
996→ value={editName}
997→ onChange={(e) => setEditName(e.target.value)}
998→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
999→ placeholder="Task name"
1000→ />
1001→ </div>
1002→ <div>
1003→ <label className="block text-sm text-muted-foreground mb-1">Description</label>
1004→ <textarea
1005→ value={editDescription}
1006→ onChange={(e) => setEditDescription(e.target.value)}
1007→ rows={3}
1008→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent resize-none"
1009→ placeholder="Task description"
1010→ />
1011→ </div>
1012→ <div>
1013→ <label className="block text-sm text-muted-foreground mb-1">Priority</label>
1014→ <select
1015→ value={editPriority}
1016→ onChange={(e) => setEditPriority(Number(e.target.value))}
1017→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
1018→ >
1019→ <option value={0}>Low</option>
1020→ <option value={1}>Medium</option>
1021→ <option value={2}>High</option>
1022→ <option value={3}>Critical</option>
1023→ </select>
1024→ </div>
1025→ <div>
1026→ <label className="block text-sm text-muted-foreground mb-1">Phase</label>
1027→ <select
1028→ value={editPhaseId ?? ''}
1029→ onChange={(e) => setEditPhaseId(e.target.value ? Number(e.target.value) : null)}
1030→ className="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
1031→ >
1032→ <option value="">No Phase</option>
1033→ {phases.map((phase) => (
1034→ <option key={phase.id} value={phase.id}>{phase.name}</option>
1035→ ))}
1036→ </select>
1037→ </div>
1038→ <div className="flex gap-2 pt-2">
1039→ <button
1040→ onClick={handleCancelEdit}
1041→ className="flex-1 px-3 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
1042→ >
1043→ Cancel
1044→ </button>
1045→ <button
1046→ onClick={handleSaveEdit}
1047→ disabled={isSaving || !editName.trim()}
1048→ 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"
1049→ >
1050→ {isSaving ? 'Saving...' : <><Check size={16} /> Save</>}
1051→ </button>
1052→ </div>
1053→ </div>
1054→ ) : (
1055→ /* View Mode */
1056→ <>
1057→ {/* Task Name */}
1058→ <div>
1059→ <label className="block text-sm text-muted-foreground mb-1">Name</label>
1060→ <p className="font-medium text-lg break-words">{selectedTask.name}</p>
1061→ </div>
1062→
1063→ {/* Description */}
1064→ <div>
1065→ <label className="block text-sm text-muted-foreground mb-1">Description</label>
1066→ <p className="text-secondary-foreground">
1067→ {selectedTask.description || <span className="italic text-muted-foreground">No description</span>}
1068→ </p>
1069→ </div>
1070→
1071→ {/* Status */}
1072→ <div>
1073→ <label className="block text-sm text-muted-foreground mb-1">Status</label>
1074→ <span className={`badge ${
1075→ selectedTask.status === 'backlog' ? 'badge-backlog' :
1076→ selectedTask.status === 'in_progress' ? 'badge-in-progress' :
1077→ selectedTask.status === 'qa' ? 'badge-qa' :
1078→ selectedTask.status === 'done' ? 'badge-done' :
1079→ 'badge-blocked'
1080→ }`}>
1081→ {STATUS_LABELS[selectedTask.status] || selectedTask.status}
1082→ </span>
1083→ </div>
1084→
1085→ {/* Priority */}
1086→ <div>
1087→ <label className="block text-sm text-muted-foreground mb-1">Priority</label>
1088→ <span className={`font-medium ${
1089→ selectedTask.priority === 3 ? 'text-destructive' :
1090→ selectedTask.priority === 2 ? 'text-warning' :
1091→ selectedTask.priority === 1 ? 'text-accent' :
1092→ 'text-muted-foreground'
1093→ }`}>
1094→ {PRIORITY_LABELS[selectedTask.priority] || 'Unknown'}
1095→ </span>
1096→ </div>
1097→
1098→ {/* Phase */}
1099→ <div>
1100→ <label className="block text-sm text-muted-foreground mb-1">Phase</label>
1101→ <p className="text-secondary-foreground">
1102→ {selectedTask.phase_id ? (
1103→ phases.find(p => p.id === selectedTask.phase_id)?.name || 'Unknown Phase'
1104→ ) : (
1105→ <span className="italic text-muted-foreground">No phase</span>
1106→ )}
1107→ </p>
1108→ </div>
1109→
1110→ {/* Dependencies */}
1111→ <div>
1112→ <label className="block text-sm text-muted-foreground mb-1 flex items-center gap-1">
1113→ <Link2 size={14} />
1114→ Dependencies
1115→ </label>
1116→ {selectedTask.depends_on && selectedTask.depends_on.length > 0 ? (
1117→ <ul className="text-secondary-foreground space-y-1">
1118→ {getDependencyNames(selectedTask.depends_on).map((name, idx) => (
1119→ <li key={idx} className="flex items-center gap-2">
1120→ <span className="w-1.5 h-1.5 rounded-full bg-primary-400" />
1121→ {name}
1122→ </li>
1123→ ))}
1124→ </ul>
1125→ ) : (
1126→ <p className="italic text-muted-foreground">No dependencies</p>
1127→ )}
1128→ </div>
1129→
1130→ {/* Agent Assignment */}
1131→ <div>
1132→ <label className="block text-sm text-muted-foreground mb-1 flex items-center gap-1">
1133→ <User size={14} />
1134→ Agent Assignment
1135→ </label>
1136→ <p className="text-secondary-foreground">
1137→ {selectedTask.agent_type ? (
1138→ <span className="capitalize">{selectedTask.agent_type.replace('_', ' ')}</span>
1139→ ) : (
1140→ <span className="italic text-muted-foreground">Not assigned</span>
1141→ )}
1142→ </p>
1143→ </div>
1144→
1145→ {/* Iteration Count */}
1146→ <div>
1147→ <label className="block text-sm text-muted-foreground mb-1 flex items-center gap-1">
1148→ <RefreshCw size={14} />
1149→ Iteration Count
1150→ </label>
1151→ <p className="text-secondary-foreground">{selectedTask.iteration_count}</p>
1152→ </div>
1153→
1154→ {/* Flagged for Review */}
1155→ <div>
1156→ <label className="block text-sm text-muted-foreground mb-1 flex items-center gap-1">
1157→ <Flag size={14} />
1158→ Flagged for Review
1159→ </label>
1160→ <p className={selectedTask.flagged_for_review ? 'text-warning' : 'text-secondary-foreground'}>
1161→ {selectedTask.flagged_for_review ? 'Yes' : 'No'}
1162→ </p>
1163→ </div>
1164→
1165→ {/* Time Estimates */}
1166→ {(selectedTask.estimated_time || selectedTask.actual_time) && (
1167→ <div>
1168→ <label className="block text-sm text-muted-foreground mb-1 flex items-center gap-1">
1169→ <Clock size={14} />
1170→ Time
1171→ </label>
1172→ <div className="text-secondary-foreground text-sm">
1173→ {selectedTask.estimated_time && <p>Estimated: {selectedTask.estimated_time} min</p>}
1174→ {selectedTask.actual_time && <p>Actual: {selectedTask.actual_time} min</p>}
1175→ </div>
1176→ </div>
1177→ )}
1178→
1179→ {/* Created/Updated timestamps */}
1180→ <div className="pt-4 border-t border-border text-xs text-muted-foreground">
1181→ <p>Created: {new Date(selectedTask.created_at).toLocaleString()}</p>
1182→ <p>Updated: {new Date(selectedTask.updated_at).toLocaleString()}</p>
1183→ </div>
1184→ </>
1185→ )}
1186→ </div>
1187→ </div>
1188→ )}
1189→
1190→ {/* Unsaved Changes Warning Dialog */}
1191→ <AlertDialog.Root open={showUnsavedDialog} onOpenChange={setShowUnsavedDialog}>
1192→ <AlertDialog.Portal>
1193→ <AlertDialog.Overlay className="fixed inset-0 bg-black/50 z-50" />
1194→ <AlertDialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-card border border-border rounded-lg p-4 md:p-6 w-[calc(100%-2rem)] max-w-md max-h-[calc(100vh-2rem)] overflow-y-auto z-50 shadow-xl">
1195→ <AlertDialog.Title className="text-xl font-semibold text-foreground">
1196→ Unsaved Changes
1197→ </AlertDialog.Title>
1198→ <AlertDialog.Description className="mt-3 text-muted-foreground">
1199→ You have unsaved changes. Are you sure you want to discard them?
1200→ </AlertDialog.Description>
1201→ <div className="mt-6 flex justify-end gap-3">
1202→ <AlertDialog.Cancel asChild>
1203→ <button
1204→ onClick={handleKeepEditing}
1205→ className="px-4 py-2 text-sm text-secondary-foreground hover:bg-secondary rounded-lg transition-colors"
1206→ >
1207→ Keep Editing
1208→ </button>
1209→ </AlertDialog.Cancel>
1210→ <AlertDialog.Action asChild>
1211→ <button
1212→ onClick={handleDiscardChanges}
1213→ className="px-4 py-2 text-sm bg-destructive hover:bg-destructive/80 text-white rounded-lg transition-colors"
1214→ >
1215→ Discard Changes
1216→ </button>
1217→ </AlertDialog.Action>
1218→ </div>
1219→ </AlertDialog.Content>
1220→ </AlertDialog.Portal>
1221→ </AlertDialog.Root>
1222→ </div>
1223→ </div>
1224→ )
1225→}
1226→
<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>