Improvement/refactor task sidebar to task modal
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user