Improvement/refactor task sidebar to task modal

This commit is contained in:
AndyMik90
2025-12-20 11:08:44 +01:00
parent 3ac3f067cc
commit 2a96f855ae
5 changed files with 581 additions and 68 deletions
+7 -5
View File
@@ -17,7 +17,7 @@ import {
} from './components/ui/tooltip';
import { Sidebar, type SidebarView } from './components/Sidebar';
import { KanbanBoard } from './components/KanbanBoard';
import { TaskDetailPanel } from './components/TaskDetailPanel';
import { TaskDetailModal } from './components/task-detail/TaskDetailModal';
import { TaskCreationWizard } from './components/TaskCreationWizard';
import { AppSettingsDialog, type AppSection } from './components/settings/AppSettings';
import type { ProjectSettingsSection } from './components/settings/ProjectSettingsContent';
@@ -469,10 +469,12 @@ export function App() {
</main>
</div>
{/* Task detail panel */}
{selectedTask && (
<TaskDetailPanel task={selectedTask} onClose={handleCloseTaskDetail} />
)}
{/* Task detail modal */}
<TaskDetailModal
open={!!selectedTask}
task={selectedTask}
onOpenChange={(open) => !open && handleCloseTaskDetail()}
/>
{/* Dialogs */}
{selectedProjectId && (
@@ -0,0 +1,517 @@
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Separator } from '../ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
import { ScrollArea } from '../ui/scroll-area';
import { TooltipProvider } from '../ui/tooltip';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
import { Progress } from '../ui/progress';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '../ui/alert-dialog';
import {
Play,
Square,
CheckCircle2,
RotateCcw,
Trash2,
Loader2,
AlertTriangle,
Pencil,
X
} from 'lucide-react';
import { cn } from '../../lib/utils';
import { calculateProgress } from '../../lib/utils';
import { startTask, stopTask, submitReview, recoverStuckTask, deleteTask } from '../../stores/task-store';
import { TASK_STATUS_LABELS } from '../../../shared/constants';
import { TaskEditDialog } from '../TaskEditDialog';
import { useTaskDetail } from './hooks/useTaskDetail';
import { TaskMetadata } from './TaskMetadata';
import { TaskWarnings } from './TaskWarnings';
import { TaskSubtasks } from './TaskSubtasks';
import { TaskLogs } from './TaskLogs';
import { TaskReview } from './TaskReview';
import type { Task } from '../../../shared/types';
interface TaskDetailModalProps {
open: boolean;
task: Task | null;
onOpenChange: (open: boolean) => void;
}
export function TaskDetailModal({ open, task, onOpenChange }: TaskDetailModalProps) {
// Don't render anything if no task
if (!task) {
return null;
}
return (
<TaskDetailModalContent
open={open}
task={task}
onOpenChange={onOpenChange}
/>
);
}
// Separate component to use hooks only when task exists
function TaskDetailModalContent({ open, task, onOpenChange }: { open: boolean; task: Task; onOpenChange: (open: boolean) => void }) {
const state = useTaskDetail({ task });
const progressPercent = calculateProgress(task.subtasks);
const completedSubtasks = task.subtasks.filter(s => s.status === 'completed').length;
const totalSubtasks = task.subtasks.length;
// Event Handlers
const handleStartStop = () => {
if (state.isRunning && !state.isStuck) {
stopTask(task.id);
} else {
startTask(task.id);
}
};
const handleRecover = async () => {
state.setIsRecovering(true);
const result = await recoverStuckTask(task.id, { autoRestart: true });
if (result.success) {
state.setIsStuck(false);
state.setHasCheckedRunning(false);
}
state.setIsRecovering(false);
};
const handleReject = async () => {
if (!state.feedback.trim()) {
return;
}
state.setIsSubmitting(true);
await submitReview(task.id, false, state.feedback);
state.setIsSubmitting(false);
state.setFeedback('');
};
const handleDelete = async () => {
state.setIsDeleting(true);
state.setDeleteError(null);
const result = await deleteTask(task.id);
if (result.success) {
state.setShowDeleteDialog(false);
onOpenChange(false);
} else {
state.setDeleteError(result.error || 'Failed to delete task');
}
state.setIsDeleting(false);
};
const handleMerge = async () => {
state.setIsMerging(true);
state.setWorkspaceError(null);
try {
const result = await window.electronAPI.mergeWorktree(task.id, { noCommit: state.stageOnly });
if (result.success && result.data?.success) {
if (state.stageOnly && result.data.staged) {
state.setWorkspaceError(null);
state.setStagedSuccess(result.data.message || 'Changes staged in main project');
state.setStagedProjectPath(result.data.projectPath);
} else {
onOpenChange(false);
}
} else {
state.setWorkspaceError(result.data?.message || result.error || 'Failed to merge changes');
}
} catch (error) {
state.setWorkspaceError(error instanceof Error ? error.message : 'Unknown error during merge');
} finally {
state.setIsMerging(false);
}
};
const handleDiscard = async () => {
state.setIsDiscarding(true);
state.setWorkspaceError(null);
const result = await window.electronAPI.discardWorktree(task.id);
if (result.success && result.data?.success) {
state.setShowDiscardDialog(false);
onOpenChange(false);
} else {
state.setWorkspaceError(result.data?.message || result.error || 'Failed to discard changes');
}
state.setIsDiscarding(false);
};
const handleClose = () => {
onOpenChange(false);
};
// Render primary action button based on state
const renderPrimaryAction = () => {
if (state.isStuck) {
return (
<Button
variant="warning"
onClick={handleRecover}
disabled={state.isRecovering}
>
{state.isRecovering ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Recovering...
</>
) : (
<>
<RotateCcw className="mr-2 h-4 w-4" />
Recover Task
</>
)}
</Button>
);
}
if (state.isIncomplete) {
return (
<Button variant="default" onClick={handleStartStop}>
<Play className="mr-2 h-4 w-4" />
Resume Task
</Button>
);
}
if (task.status === 'backlog' || task.status === 'in_progress') {
return (
<Button
variant={state.isRunning ? 'destructive' : 'default'}
onClick={handleStartStop}
>
{state.isRunning ? (
<>
<Square className="mr-2 h-4 w-4" />
Stop Task
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Start Task
</>
)}
</Button>
);
}
if (task.status === 'done') {
return (
<div className="completion-state text-sm flex items-center gap-2 text-success">
<CheckCircle2 className="h-5 w-5" />
<span className="font-medium">Task completed</span>
</div>
);
}
return null;
};
return (
<TooltipProvider delayDuration={300}>
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Portal>
{/* Semi-transparent overlay - can see background content */}
<DialogPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/60',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0'
)}
/>
{/* Full-height centered modal content */}
<DialogPrimitive.Content
className={cn(
'fixed left-[50%] top-4 z-50',
'translate-x-[-50%]',
'w-[95vw] max-w-5xl h-[calc(100vh-32px)]',
'bg-card border border-border rounded-xl',
'shadow-2xl overflow-hidden flex flex-col',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'duration-200'
)}
>
{/* Header */}
<div className="p-5 pb-4 border-b border-border shrink-0">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<DialogPrimitive.Title className="text-xl font-semibold leading-tight text-foreground pr-4">
{task.title}
</DialogPrimitive.Title>
<DialogPrimitive.Description asChild>
<div className="mt-2.5 flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-xs font-mono">
{task.specId}
</Badge>
{state.isStuck ? (
<Badge variant="warning" className="text-xs flex items-center gap-1 animate-pulse">
<AlertTriangle className="h-3 w-3" />
Stuck
</Badge>
) : state.isIncomplete ? (
<>
<Badge variant="warning" className="text-xs flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
Incomplete
</Badge>
</>
) : (
<>
<Badge
variant={task.status === 'done' ? 'success' : task.status === 'human_review' ? 'purple' : task.status === 'in_progress' ? 'info' : 'secondary'}
className={cn('text-xs', (task.status === 'in_progress' && !state.isStuck) && 'status-running')}
>
{TASK_STATUS_LABELS[task.status]}
</Badge>
{task.status === 'human_review' && task.reviewReason && (
<Badge
variant={task.reviewReason === 'completed' ? 'success' : task.reviewReason === 'errors' ? 'destructive' : 'warning'}
className="text-xs"
>
{task.reviewReason === 'completed' ? 'Completed' :
task.reviewReason === 'errors' ? 'Has Errors' :
task.reviewReason === 'plan_review' ? 'Approve Plan' : 'QA Issues'}
</Badge>
)}
</>
)}
{/* Compact progress indicator */}
{totalSubtasks > 0 && (
<span className="text-xs text-muted-foreground ml-1">
{completedSubtasks}/{totalSubtasks} subtasks
</span>
)}
</div>
</DialogPrimitive.Description>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost"
size="icon"
className="hover:bg-primary/10 hover:text-primary transition-colors"
onClick={() => state.setIsEditDialogOpen(true)}
disabled={state.isRunning && !state.isStuck}
>
<Pencil className="h-4 w-4" />
</Button>
<DialogPrimitive.Close asChild>
<Button
variant="ghost"
size="icon"
className="hover:bg-muted transition-colors"
>
<X className="h-5 w-5" />
<span className="sr-only">Close</span>
</Button>
</DialogPrimitive.Close>
</div>
</div>
{/* Progress bar - only show when running or has progress */}
{(state.isRunning || completedSubtasks > 0) && totalSubtasks > 0 && (
<div className="mt-3 flex items-center gap-3">
<Progress value={progressPercent} className="h-1.5 flex-1" />
<span className="text-xs text-muted-foreground tabular-nums w-10 text-right">{progressPercent}%</span>
</div>
)}
{/* Warnings - compact inline */}
{(state.isStuck || state.isIncomplete) && (
<div className="mt-3">
<TaskWarnings
isStuck={state.isStuck}
isIncomplete={state.isIncomplete}
isRecovering={state.isRecovering}
taskProgress={state.taskProgress}
onRecover={handleRecover}
onResume={handleStartStop}
/>
</div>
)}
</div>
{/* Body - Single Column with Tabs */}
<div className="flex-1 min-h-0 overflow-hidden">
<Tabs value={state.activeTab} onValueChange={state.setActiveTab} className="flex flex-col h-full">
<TabsList className="w-full justify-start rounded-none border-b border-border bg-transparent px-5 h-auto shrink-0">
<TabsTrigger
value="overview"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2.5 text-sm"
>
Overview
</TabsTrigger>
<TabsTrigger
value="subtasks"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2.5 text-sm"
>
Subtasks ({task.subtasks.length})
</TabsTrigger>
<TabsTrigger
value="logs"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2.5 text-sm"
>
Logs
</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="flex-1 min-h-0 overflow-hidden mt-0">
<ScrollArea className="h-full">
<div className="p-5 space-y-5">
{/* Metadata */}
<TaskMetadata task={task} />
{/* Human Review Section */}
{state.needsReview && (
<>
<Separator />
<TaskReview
task={task}
feedback={state.feedback}
isSubmitting={state.isSubmitting}
worktreeStatus={state.worktreeStatus}
worktreeDiff={state.worktreeDiff}
isLoadingWorktree={state.isLoadingWorktree}
isMerging={state.isMerging}
isDiscarding={state.isDiscarding}
showDiscardDialog={state.showDiscardDialog}
showDiffDialog={state.showDiffDialog}
workspaceError={state.workspaceError}
stageOnly={state.stageOnly}
stagedSuccess={state.stagedSuccess}
stagedProjectPath={state.stagedProjectPath}
mergePreview={state.mergePreview}
isLoadingPreview={state.isLoadingPreview}
showConflictDialog={state.showConflictDialog}
onFeedbackChange={state.setFeedback}
onReject={handleReject}
onMerge={handleMerge}
onDiscard={handleDiscard}
onShowDiscardDialog={state.setShowDiscardDialog}
onShowDiffDialog={state.setShowDiffDialog}
onStageOnlyChange={state.setStageOnly}
onShowConflictDialog={state.setShowConflictDialog}
onLoadMergePreview={state.loadMergePreview}
/>
</>
)}
</div>
</ScrollArea>
</TabsContent>
{/* Subtasks Tab */}
<TabsContent value="subtasks" className="flex-1 min-h-0 overflow-hidden mt-0">
<TaskSubtasks task={task} />
</TabsContent>
{/* Logs Tab */}
<TabsContent value="logs" className="flex-1 min-h-0 overflow-hidden mt-0">
<TaskLogs
task={task}
phaseLogs={state.phaseLogs}
isLoadingLogs={state.isLoadingLogs}
expandedPhases={state.expandedPhases}
isStuck={state.isStuck}
logsEndRef={state.logsEndRef}
logsContainerRef={state.logsContainerRef}
onLogsScroll={state.handleLogsScroll}
onTogglePhase={state.togglePhase}
/>
</TabsContent>
</Tabs>
</div>
{/* Footer - Actions */}
<div className="flex items-center gap-3 px-5 py-3 border-t border-border shrink-0">
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => state.setShowDeleteDialog(true)}
disabled={state.isRunning && !state.isStuck}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Task
</Button>
<div className="flex-1" />
{renderPrimaryAction()}
<Button variant="outline" onClick={handleClose}>
Close
</Button>
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
{/* Edit Task Dialog */}
<TaskEditDialog
task={task}
open={state.isEditDialogOpen}
onOpenChange={state.setIsEditDialogOpen}
/>
{/* Delete Confirmation Dialog */}
<AlertDialog open={state.showDeleteDialog} onOpenChange={state.setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
Delete Task
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-sm text-muted-foreground space-y-3">
<p>
Are you sure you want to delete <strong className="text-foreground">"{task.title}"</strong>?
</p>
<p className="text-destructive">
This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project.
</p>
{state.deleteError && (
<p className="text-destructive bg-destructive/10 px-3 py-2 rounded-lg text-sm">
{state.deleteError}
</p>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={state.isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDelete();
}}
disabled={state.isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{state.isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Permanently
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TooltipProvider>
);
}
@@ -337,7 +337,7 @@ function LogEntry({ entry }: LogEntryProps) {
<Icon className="h-3 w-3 animate-pulse" />
<span className="font-medium">{label}</span>
{entry.tool_input && (
<span className="text-muted-foreground truncate max-w-[200px]" title={entry.tool_input}>
<span className="text-muted-foreground truncate max-w-[500px]" title={entry.tool_input}>
{entry.tool_input}
</span>
)}
@@ -1,5 +1,4 @@
import {
Info,
Target,
Bug,
Wrench,
@@ -47,18 +46,24 @@ interface TaskMetadataProps {
}
export function TaskMetadata({ task }: TaskMetadataProps) {
const hasClassification = task.metadata && (
task.metadata.category ||
task.metadata.priority ||
task.metadata.complexity ||
task.metadata.impact ||
task.metadata.securitySeverity ||
task.metadata.sourceType
);
return (
<div className="space-y-5">
{/* Classification Badges */}
{task.metadata && (
<div>
<div className="section-divider mb-3">
<Info className="h-3 w-3" />
Classification
</div>
<div className="flex flex-wrap gap-1.5">
{/* Compact Metadata Bar: Classification + Timeline */}
<div className="flex flex-wrap items-center justify-between gap-3 pb-4 border-b border-border">
{/* Classification Badges - Left */}
{hasClassification && (
<div className="flex flex-wrap items-center gap-1.5">
{/* Category */}
{task.metadata.category && (
{task.metadata?.category && (
<Badge
variant="outline"
className={cn('text-xs', TASK_CATEGORY_COLORS[task.metadata.category])}
@@ -71,7 +76,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</Badge>
)}
{/* Priority */}
{task.metadata.priority && (
{task.metadata?.priority && (
<Badge
variant="outline"
className={cn('text-xs', TASK_PRIORITY_COLORS[task.metadata.priority])}
@@ -80,7 +85,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</Badge>
)}
{/* Complexity */}
{task.metadata.complexity && (
{task.metadata?.complexity && (
<Badge
variant="outline"
className={cn('text-xs', TASK_COMPLEXITY_COLORS[task.metadata.complexity])}
@@ -89,7 +94,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</Badge>
)}
{/* Impact */}
{task.metadata.impact && (
{task.metadata?.impact && (
<Badge
variant="outline"
className={cn('text-xs', TASK_IMPACT_COLORS[task.metadata.impact])}
@@ -98,17 +103,17 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</Badge>
)}
{/* Security Severity */}
{task.metadata.securitySeverity && (
{task.metadata?.securitySeverity && (
<Badge
variant="outline"
className={cn('text-xs', TASK_IMPACT_COLORS[task.metadata.securitySeverity])}
>
<Shield className="h-3 w-3 mr-1" />
{task.metadata.securitySeverity} severity
{task.metadata.securitySeverity}
</Badge>
)}
{/* Source Type */}
{task.metadata.sourceType && (
{task.metadata?.sourceType && (
<Badge variant="secondary" className="text-xs">
{task.metadata.sourceType === 'ideation' && task.metadata.ideationType
? IDEATION_TYPE_LABELS[task.metadata.ideationType] || task.metadata.ideationType
@@ -116,66 +121,72 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</Badge>
)}
</div>
</div>
)}
)}
{/* Description */}
{/* Timeline - Right */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1.5">
<Clock className="h-3 w-3" />
Created {formatRelativeTime(task.createdAt)}
</span>
<span className="text-border"></span>
<span>Updated {formatRelativeTime(task.updatedAt)}</span>
</div>
</div>
{/* Description - Primary Content */}
{task.description && (
<div>
<div className="section-divider mb-3">
<FileCode className="h-3 w-3" />
Description
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
{sanitizeMarkdownForDisplay(task.description, 500)}
<p className="text-sm text-foreground/90 leading-relaxed">
{sanitizeMarkdownForDisplay(task.description, 800)}
</p>
</div>
)}
{/* Metadata Details */}
{/* Secondary Details */}
{task.metadata && (
<div className="space-y-4">
<div className="space-y-4 pt-2">
{/* Rationale */}
{task.metadata.rationale && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<Lightbulb className="h-3.5 w-3.5 text-warning" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<Lightbulb className="h-3 w-3 text-warning" />
Rationale
</h3>
<p className="text-sm text-muted-foreground">{task.metadata.rationale}</p>
<p className="text-sm text-foreground/80">{task.metadata.rationale}</p>
</div>
)}
{/* Problem Solved */}
{task.metadata.problemSolved && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<Target className="h-3.5 w-3.5 text-success" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<Target className="h-3 w-3 text-success" />
Problem Solved
</h3>
<p className="text-sm text-muted-foreground">{task.metadata.problemSolved}</p>
<p className="text-sm text-foreground/80">{task.metadata.problemSolved}</p>
</div>
)}
{/* Target Audience */}
{task.metadata.targetAudience && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<Users className="h-3.5 w-3.5 text-info" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<Users className="h-3 w-3 text-info" />
Target Audience
</h3>
<p className="text-sm text-muted-foreground">{task.metadata.targetAudience}</p>
<p className="text-sm text-foreground/80">{task.metadata.targetAudience}</p>
</div>
)}
{/* Dependencies */}
{task.metadata.dependencies && task.metadata.dependencies.length > 0 && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<GitBranch className="h-3.5 w-3.5 text-purple-400" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<GitBranch className="h-3 w-3 text-purple-400" />
Dependencies
</h3>
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-0.5">
<ul className="text-sm text-foreground/80 list-disc list-inside space-y-0.5">
{task.metadata.dependencies.map((dep, idx) => (
<li key={idx}>{dep}</li>
))}
@@ -186,11 +197,11 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
{/* Acceptance Criteria */}
{task.metadata.acceptanceCriteria && task.metadata.acceptanceCriteria.length > 0 && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<ListChecks className="h-3.5 w-3.5 text-success" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<ListChecks className="h-3 w-3 text-success" />
Acceptance Criteria
</h3>
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-0.5">
<ul className="text-sm text-foreground/80 list-disc list-inside space-y-0.5">
{task.metadata.acceptanceCriteria.map((criteria, idx) => (
<li key={idx}>{criteria}</li>
))}
@@ -201,8 +212,8 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
{/* Affected Files */}
{task.metadata.affectedFiles && task.metadata.affectedFiles.length > 0 && (
<div>
<h3 className="text-sm font-medium text-foreground mb-1.5 flex items-center gap-1.5">
<FileCode className="h-3.5 w-3.5 text-muted-foreground" />
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1.5 flex items-center gap-1.5">
<FileCode className="h-3 w-3" />
Affected Files
</h3>
<div className="flex flex-wrap gap-1">
@@ -223,24 +234,6 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
)}
</div>
)}
{/* Timestamps */}
<div>
<div className="section-divider mb-3">
<Clock className="h-3 w-3" />
Timeline
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Created</span>
<span className="text-foreground tabular-nums">{formatRelativeTime(task.createdAt)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Updated</span>
<span className="text-foreground tabular-nums">{formatRelativeTime(task.updatedAt)}</span>
</div>
</div>
</div>
</div>
);
}
@@ -1,4 +1,5 @@
export { TaskDetailPanel } from './TaskDetailPanel';
export { TaskDetailModal } from './TaskDetailModal';
export { TaskHeader } from './TaskHeader';
export { TaskProgress } from './TaskProgress';
export { TaskMetadata } from './TaskMetadata';