From 2a96f855ae8b758a9c15dddaa2637bfd3bf0ce49 Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Sat, 20 Dec 2025 11:08:44 +0100 Subject: [PATCH] Improvement/refactor task sidebar to task modal --- auto-claude-ui/src/renderer/App.tsx | 12 +- .../task-detail/TaskDetailModal.tsx | 517 ++++++++++++++++++ .../components/task-detail/TaskLogs.tsx | 2 +- .../components/task-detail/TaskMetadata.tsx | 117 ++-- .../renderer/components/task-detail/index.ts | 1 + 5 files changed, 581 insertions(+), 68 deletions(-) create mode 100644 auto-claude-ui/src/renderer/components/task-detail/TaskDetailModal.tsx diff --git a/auto-claude-ui/src/renderer/App.tsx b/auto-claude-ui/src/renderer/App.tsx index 383344cf..fed74cad 100644 --- a/auto-claude-ui/src/renderer/App.tsx +++ b/auto-claude-ui/src/renderer/App.tsx @@ -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() { - {/* Task detail panel */} - {selectedTask && ( - - )} + {/* Task detail modal */} + !open && handleCloseTaskDetail()} + /> {/* Dialogs */} {selectedProjectId && ( diff --git a/auto-claude-ui/src/renderer/components/task-detail/TaskDetailModal.tsx b/auto-claude-ui/src/renderer/components/task-detail/TaskDetailModal.tsx new file mode 100644 index 00000000..6163e9f6 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/task-detail/TaskDetailModal.tsx @@ -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 ( + + ); +} + +// 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 ( + + ); + } + + if (state.isIncomplete) { + return ( + + ); + } + + if (task.status === 'backlog' || task.status === 'in_progress') { + return ( + + ); + } + + if (task.status === 'done') { + return ( +
+ + Task completed +
+ ); + } + + return null; + }; + + + return ( + + + + {/* Semi-transparent overlay - can see background content */} + + + {/* Full-height centered modal content */} + + {/* Header */} +
+
+
+ + {task.title} + + +
+ + {task.specId} + + {state.isStuck ? ( + + + Stuck + + ) : state.isIncomplete ? ( + <> + + + Incomplete + + + ) : ( + <> + + {TASK_STATUS_LABELS[task.status]} + + {task.status === 'human_review' && task.reviewReason && ( + + {task.reviewReason === 'completed' ? 'Completed' : + task.reviewReason === 'errors' ? 'Has Errors' : + task.reviewReason === 'plan_review' ? 'Approve Plan' : 'QA Issues'} + + )} + + )} + {/* Compact progress indicator */} + {totalSubtasks > 0 && ( + + {completedSubtasks}/{totalSubtasks} subtasks + + )} +
+
+
+
+ + + + +
+
+ + {/* Progress bar - only show when running or has progress */} + {(state.isRunning || completedSubtasks > 0) && totalSubtasks > 0 && ( +
+ + {progressPercent}% +
+ )} + + {/* Warnings - compact inline */} + {(state.isStuck || state.isIncomplete) && ( +
+ +
+ )} +
+ + {/* Body - Single Column with Tabs */} +
+ + + + Overview + + + Subtasks ({task.subtasks.length}) + + + Logs + + + + {/* Overview Tab */} + + +
+ {/* Metadata */} + + + {/* Human Review Section */} + {state.needsReview && ( + <> + + + + )} +
+
+
+ + {/* Subtasks Tab */} + + + + + {/* Logs Tab */} + + + +
+
+ + {/* Footer - Actions */} +
+ +
+ {renderPrimaryAction()} + +
+ + + + + {/* Edit Task Dialog */} + + + {/* Delete Confirmation Dialog */} + + + + + + Delete Task + + +
+

+ Are you sure you want to delete "{task.title}"? +

+

+ This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project. +

+ {state.deleteError && ( +

+ {state.deleteError} +

+ )} +
+
+
+ + Cancel + { + e.preventDefault(); + handleDelete(); + }} + disabled={state.isDeleting} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + {state.isDeleting ? ( + <> + + Deleting... + + ) : ( + <> + + Delete Permanently + + )} + + +
+
+ + ); +} diff --git a/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx b/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx index 9f5a9ed9..3481b263 100644 --- a/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx +++ b/auto-claude-ui/src/renderer/components/task-detail/TaskLogs.tsx @@ -337,7 +337,7 @@ function LogEntry({ entry }: LogEntryProps) { {label} {entry.tool_input && ( - + {entry.tool_input} )} diff --git a/auto-claude-ui/src/renderer/components/task-detail/TaskMetadata.tsx b/auto-claude-ui/src/renderer/components/task-detail/TaskMetadata.tsx index 2472a0ed..7c6588c0 100644 --- a/auto-claude-ui/src/renderer/components/task-detail/TaskMetadata.tsx +++ b/auto-claude-ui/src/renderer/components/task-detail/TaskMetadata.tsx @@ -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 (
- {/* Classification Badges */} - {task.metadata && ( -
-
- - Classification -
-
+ {/* Compact Metadata Bar: Classification + Timeline */} +
+ {/* Classification Badges - Left */} + {hasClassification && ( +
{/* Category */} - {task.metadata.category && ( + {task.metadata?.category && ( )} {/* Priority */} - {task.metadata.priority && ( + {task.metadata?.priority && ( )} {/* Complexity */} - {task.metadata.complexity && ( + {task.metadata?.complexity && ( )} {/* Impact */} - {task.metadata.impact && ( + {task.metadata?.impact && ( )} {/* Security Severity */} - {task.metadata.securitySeverity && ( + {task.metadata?.securitySeverity && ( - {task.metadata.securitySeverity} severity + {task.metadata.securitySeverity} )} {/* Source Type */} - {task.metadata.sourceType && ( + {task.metadata?.sourceType && ( {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) { )}
-
- )} + )} - {/* Description */} + {/* Timeline - Right */} +
+ + + Created {formatRelativeTime(task.createdAt)} + + + Updated {formatRelativeTime(task.updatedAt)} +
+
+ + {/* Description - Primary Content */} {task.description && (
-
- - Description -
-

- {sanitizeMarkdownForDisplay(task.description, 500)} +

+ {sanitizeMarkdownForDisplay(task.description, 800)}

)} - {/* Metadata Details */} + {/* Secondary Details */} {task.metadata && ( -
+
{/* Rationale */} {task.metadata.rationale && (
-

- +

+ Rationale

-

{task.metadata.rationale}

+

{task.metadata.rationale}

)} {/* Problem Solved */} {task.metadata.problemSolved && (
-

- +

+ Problem Solved

-

{task.metadata.problemSolved}

+

{task.metadata.problemSolved}

)} {/* Target Audience */} {task.metadata.targetAudience && (
-

- +

+ Target Audience

-

{task.metadata.targetAudience}

+

{task.metadata.targetAudience}

)} {/* Dependencies */} {task.metadata.dependencies && task.metadata.dependencies.length > 0 && (
-

- +

+ Dependencies

-
    +
      {task.metadata.dependencies.map((dep, idx) => (
    • {dep}
    • ))} @@ -186,11 +197,11 @@ export function TaskMetadata({ task }: TaskMetadataProps) { {/* Acceptance Criteria */} {task.metadata.acceptanceCriteria && task.metadata.acceptanceCriteria.length > 0 && (
      -

      - +

      + Acceptance Criteria

      -
        +
          {task.metadata.acceptanceCriteria.map((criteria, idx) => (
        • {criteria}
        • ))} @@ -201,8 +212,8 @@ export function TaskMetadata({ task }: TaskMetadataProps) { {/* Affected Files */} {task.metadata.affectedFiles && task.metadata.affectedFiles.length > 0 && (
          -

          - +

          + Affected Files

          @@ -223,24 +234,6 @@ export function TaskMetadata({ task }: TaskMetadataProps) { )}
          )} - - {/* Timestamps */} -
          -
          - - Timeline -
          -
          -
          - Created - {formatRelativeTime(task.createdAt)} -
          -
          - Updated - {formatRelativeTime(task.updatedAt)} -
          -
          -

          ); } diff --git a/auto-claude-ui/src/renderer/components/task-detail/index.ts b/auto-claude-ui/src/renderer/components/task-detail/index.ts index 49b0fe5f..102209fa 100644 --- a/auto-claude-ui/src/renderer/components/task-detail/index.ts +++ b/auto-claude-ui/src/renderer/components/task-detail/index.ts @@ -1,4 +1,5 @@ export { TaskDetailPanel } from './TaskDetailPanel'; +export { TaskDetailModal } from './TaskDetailModal'; export { TaskHeader } from './TaskHeader'; export { TaskProgress } from './TaskProgress'; export { TaskMetadata } from './TaskMetadata';