diff --git a/frontend/src/components/context/TaskDetailsContextProvider.tsx b/frontend/src/components/context/TaskDetailsContextProvider.tsx new file mode 100644 index 00000000..0b859555 --- /dev/null +++ b/frontend/src/components/context/TaskDetailsContextProvider.tsx @@ -0,0 +1,425 @@ +import { + Dispatch, + FC, + ReactNode, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import type { + ApiResponse, + AttemptData, + EditorType, + ExecutionProcess, + ExecutionProcessSummary, + TaskAttempt, + TaskAttemptActivityWithPrompt, + TaskAttemptState, + TaskWithAttemptStatus, + WorktreeDiff, +} from 'shared/types.ts'; +import { makeRequest } from '@/lib/api.ts'; +import { TaskDetailsContext } from './taskDetailsContext.ts'; + +const TaskDetailsProvider: FC<{ + task: TaskWithAttemptStatus; + projectId: string; + children: ReactNode; + activeTab: 'logs' | 'diffs'; + setActiveTab: Dispatch>; + setShowEditorDialog: Dispatch>; + isOpen: boolean; + userSelectedTab: boolean; +}> = ({ + task, + projectId, + children, + activeTab, + setActiveTab, + setShowEditorDialog, + isOpen, + userSelectedTab, +}) => { + const [loading, setLoading] = useState(false); + const [isStopping, setIsStopping] = useState(false); + const [selectedAttempt, setSelectedAttempt] = useState( + null + ); + const [deletingFiles, setDeletingFiles] = useState>(new Set()); + const [fileToDelete, setFileToDelete] = useState(null); + + // Diff-related state + const [diff, setDiff] = useState(null); + const [diffLoading, setDiffLoading] = useState(true); + const [diffError, setDiffError] = useState(null); + const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false); + + const [executionState, setExecutionState] = useState( + null + ); + + const [attemptData, setAttemptData] = useState({ + activities: [], + processes: [], + runningProcessDetails: {}, + }); + + const diffLoadingRef = useRef(false); + + const fetchDiff = useCallback( + async (isBackgroundRefresh = false) => { + if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) { + setDiff(null); + setDiffLoading(false); + return; + } + + // Prevent multiple concurrent requests + if (diffLoadingRef.current) { + return; + } + + try { + diffLoadingRef.current = true; + if (isBackgroundRefresh) { + setIsBackgroundRefreshing(true); + } else { + setDiffLoading(true); + } + setDiffError(null); + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/diff` + ); + + if (response.ok) { + const result: ApiResponse = await response.json(); + if (result.success && result.data) { + setDiff(result.data); + } else { + setDiffError('Failed to load diff'); + } + } else { + setDiffError('Failed to load diff'); + } + } catch (err) { + setDiffError('Failed to load diff'); + } finally { + diffLoadingRef.current = false; + if (isBackgroundRefresh) { + setIsBackgroundRefreshing(false); + } else { + setDiffLoading(false); + } + } + }, + [projectId, selectedAttempt?.id, selectedAttempt?.task_id] + ); + + useEffect(() => { + if (isOpen) { + fetchDiff(); + } + }, [isOpen, fetchDiff]); + + const fetchExecutionState = useCallback( + async (attemptId: string, taskId: string) => { + if (!task) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}` + ); + + if (response.ok) { + const result: ApiResponse = await response.json(); + if (result.success && result.data) { + setExecutionState(result.data); + } + } + } catch (err) { + console.error('Failed to fetch execution state:', err); + } + }, + [task, projectId] + ); + + const handleOpenInEditor = useCallback( + async (editorType?: EditorType) => { + if (!task || !selectedAttempt) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/open-editor`, + { + method: 'POST', + body: JSON.stringify( + editorType ? { editor_type: editorType } : null + ), + } + ); + + if (!response.ok) { + if (!editorType) { + setShowEditorDialog(true); + } + } + } catch (err) { + console.error('Failed to open editor:', err); + if (!editorType) { + setShowEditorDialog(true); + } + } + }, + [task, projectId, selectedAttempt, setShowEditorDialog] + ); + + const fetchAttemptData = useCallback( + async (attemptId: string, taskId: string) => { + if (!task) return; + + try { + const [activitiesResponse, processesResponse] = await Promise.all([ + makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/activities` + ), + makeRequest( + `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/execution-processes` + ), + ]); + + if (activitiesResponse.ok && processesResponse.ok) { + const activitiesResult: ApiResponse = + await activitiesResponse.json(); + const processesResult: ApiResponse = + await processesResponse.json(); + + if ( + activitiesResult.success && + processesResult.success && + activitiesResult.data && + processesResult.data + ) { + const runningActivities = activitiesResult.data.filter( + (activity) => + activity.status === 'setuprunning' || + activity.status === 'executorrunning' + ); + + const runningProcessDetails: Record = {}; + + // Fetch details for running activities + for (const activity of runningActivities) { + try { + const detailResponse = await makeRequest( + `/api/projects/${projectId}/execution-processes/${activity.execution_process_id}` + ); + if (detailResponse.ok) { + const detailResult: ApiResponse = + await detailResponse.json(); + if (detailResult.success && detailResult.data) { + runningProcessDetails[activity.execution_process_id] = + detailResult.data; + } + } + } catch (err) { + console.error( + `Failed to fetch execution process ${activity.execution_process_id}:`, + err + ); + } + } + + // Also fetch setup script process details if it exists in the processes + const setupProcess = processesResult.data.find( + (process) => process.process_type === 'setupscript' + ); + if (setupProcess && !runningProcessDetails[setupProcess.id]) { + try { + const detailResponse = await makeRequest( + `/api/projects/${projectId}/execution-processes/${setupProcess.id}` + ); + if (detailResponse.ok) { + const detailResult: ApiResponse = + await detailResponse.json(); + if (detailResult.success && detailResult.data) { + runningProcessDetails[setupProcess.id] = detailResult.data; + } + } + } catch (err) { + console.error( + `Failed to fetch setup process details ${setupProcess.id}:`, + err + ); + } + } + + setAttemptData({ + activities: activitiesResult.data, + processes: processesResult.data, + runningProcessDetails, + }); + } + } + } catch (err) { + console.error('Failed to fetch attempt data:', err); + } + }, + [task, projectId] + ); + + useEffect(() => { + if (selectedAttempt && task) { + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + fetchExecutionState(selectedAttempt.id, selectedAttempt.task_id); + } + }, [selectedAttempt, task, fetchAttemptData, fetchExecutionState]); + + const isAttemptRunning = useMemo(() => { + if (!selectedAttempt || isStopping) { + return false; + } + + return attemptData.processes.some( + (process) => + (process.process_type === 'codingagent' || + process.process_type === 'setupscript') && + process.status === 'running' + ); + }, [selectedAttempt, attemptData.processes, isStopping]); + + useEffect(() => { + if (!isAttemptRunning || !task) return; + + const interval = setInterval(() => { + if (selectedAttempt) { + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + fetchExecutionState(selectedAttempt.id, selectedAttempt.task_id); + } + }, 2000); + + return () => clearInterval(interval); + }, [ + isAttemptRunning, + task, + selectedAttempt, + fetchAttemptData, + fetchExecutionState, + ]); + + // Refresh diff when coding agent is running and making changes + useEffect(() => { + if (!executionState || !isOpen || !selectedAttempt) return; + + const isCodingAgentRunning = + executionState.execution_state === 'CodingAgentRunning'; + + if (isCodingAgentRunning) { + // Immediately refresh diff when coding agent starts running + fetchDiff(true); + + // Then refresh diff every 2 seconds while coding agent is active + const interval = setInterval(() => { + fetchDiff(true); + }, 2000); + + return () => { + clearInterval(interval); + }; + } + }, [executionState, isOpen, selectedAttempt, fetchDiff]); + + // Refresh diff when coding agent completes or changes state + useEffect(() => { + if (!executionState?.execution_state || !isOpen || !selectedAttempt) return; + + const isCodingAgentComplete = + executionState.execution_state === 'CodingAgentComplete'; + const isCodingAgentFailed = + executionState.execution_state === 'CodingAgentFailed'; + const isComplete = executionState.execution_state === 'Complete'; + const hasChanges = executionState.has_changes; + + // Fetch diff when coding agent completes, fails, or task is complete and has changes + if ( + (isCodingAgentComplete || isCodingAgentFailed || isComplete) && + hasChanges + ) { + fetchDiff(); + // Auto-switch to diffs tab when changes are detected, but only if user hasn't manually selected a tab + if (activeTab === 'logs' && !userSelectedTab) { + setActiveTab('diffs'); + } + } + }, [ + executionState?.execution_state, + executionState?.has_changes, + isOpen, + selectedAttempt, + fetchDiff, + activeTab, + userSelectedTab, + setActiveTab, + ]); + + const value = useMemo( + () => ({ + task, + projectId, + loading, + setLoading, + selectedAttempt, + setSelectedAttempt, + isStopping, + setIsStopping, + deletingFiles, + fileToDelete, + setFileToDelete, + setDeletingFiles, + fetchDiff, + setDiffError, + diff, + diffError, + diffLoading, + setDiffLoading, + setDiff, + isBackgroundRefreshing, + handleOpenInEditor, + isAttemptRunning, + fetchExecutionState, + executionState, + attemptData, + setAttemptData, + fetchAttemptData, + }), + [ + task, + projectId, + loading, + selectedAttempt, + isStopping, + deletingFiles, + fileToDelete, + fetchDiff, + diff, + diffError, + diffLoading, + isBackgroundRefreshing, + handleOpenInEditor, + isAttemptRunning, + fetchExecutionState, + executionState, + attemptData, + fetchAttemptData, + ] + ); + return ( + + {children} + + ); +}; + +export default TaskDetailsProvider; diff --git a/frontend/src/components/context/taskDetailsContext.ts b/frontend/src/components/context/taskDetailsContext.ts new file mode 100644 index 00000000..24fc53a4 --- /dev/null +++ b/frontend/src/components/context/taskDetailsContext.ts @@ -0,0 +1,46 @@ +import { createContext, Dispatch, SetStateAction } from 'react'; +import type { + AttemptData, + EditorType, + TaskAttempt, + TaskAttemptState, + TaskWithAttemptStatus, + WorktreeDiff, +} from 'shared/types.ts'; + +export interface TaskDetailsContextValue { + task: TaskWithAttemptStatus; + projectId: string; + loading: boolean; + setLoading: Dispatch>; + selectedAttempt: TaskAttempt | null; + setSelectedAttempt: Dispatch>; + isStopping: boolean; + setIsStopping: Dispatch>; + deletingFiles: Set; + setDeletingFiles: Dispatch>>; + fileToDelete: string | null; + setFileToDelete: Dispatch>; + setDiffError: Dispatch>; + fetchDiff: (isBackgroundRefresh?: boolean) => Promise; + diff: WorktreeDiff | null; + diffError: string | null; + diffLoading: boolean; + isBackgroundRefreshing: boolean; + setDiff: Dispatch>; + setDiffLoading: Dispatch>; + handleOpenInEditor: (editorType?: EditorType) => Promise; + isAttemptRunning: boolean; + fetchExecutionState: ( + attemptId: string, + taskId: string + ) => Promise | void; + executionState: TaskAttemptState | null; + attemptData: AttemptData; + setAttemptData: Dispatch>; + fetchAttemptData: (attemptId: string, taskId: string) => Promise | void; +} + +export const TaskDetailsContext = createContext( + {} as TaskDetailsContextValue +); diff --git a/frontend/src/components/projects/project-detail.tsx b/frontend/src/components/projects/project-detail.tsx index e0addb87..b3de6c10 100644 --- a/frontend/src/components/projects/project-detail.tsx +++ b/frontend/src/components/projects/project-detail.tsx @@ -99,7 +99,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) { if (error || !project) { return ( -
+
+ + + + + ); +} + +export default DeleteFileConfirmationDialog; diff --git a/frontend/src/components/tasks/EditorSelectionDialog.tsx b/frontend/src/components/tasks/EditorSelectionDialog.tsx index 8025767d..151531d1 100644 --- a/frontend/src/components/tasks/EditorSelectionDialog.tsx +++ b/frontend/src/components/tasks/EditorSelectionDialog.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useContext, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Dialog, @@ -16,11 +16,11 @@ import { SelectValue, } from '@/components/ui/select'; import type { EditorType } from 'shared/types'; +import { TaskDetailsContext } from '@/components/context/taskDetailsContext.ts'; interface EditorSelectionDialogProps { isOpen: boolean; onClose: () => void; - onSelectEditor: (editorType: EditorType) => void; } const editorOptions: { @@ -63,12 +63,12 @@ const editorOptions: { export function EditorSelectionDialog({ isOpen, onClose, - onSelectEditor, }: EditorSelectionDialogProps) { + const { handleOpenInEditor } = useContext(TaskDetailsContext); const [selectedEditor, setSelectedEditor] = useState('vscode'); const handleConfirm = () => { - onSelectEditor(selectedEditor); + handleOpenInEditor(selectedEditor); onClose(); }; diff --git a/frontend/src/components/tasks/TaskActivityHistory.tsx b/frontend/src/components/tasks/TaskActivityHistory.tsx index ad0e6451..3b76c151 100644 --- a/frontend/src/components/tasks/TaskActivityHistory.tsx +++ b/frontend/src/components/tasks/TaskActivityHistory.tsx @@ -1,21 +1,20 @@ import { useState } from 'react'; -import { Clock, ChevronDown, ChevronUp, Code } from 'lucide-react'; +import { ChevronDown, ChevronUp, Clock, Code } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Chip } from '@/components/ui/chip'; -import { NormalizedConversationViewer } from './NormalizedConversationViewer'; +import { NormalizedConversationViewer } from './TaskDetails/NormalizedConversationViewer.tsx'; import type { + ExecutionProcess, TaskAttempt, TaskAttemptActivityWithPrompt, TaskAttemptStatus, - ExecutionProcess, } from 'shared/types'; interface TaskActivityHistoryProps { selectedAttempt: TaskAttempt | null; activities: TaskAttemptActivityWithPrompt[]; runningProcessDetails: Record; - projectId: string; } const getAttemptStatusDisplay = ( @@ -64,7 +63,6 @@ export function TaskActivityHistory({ selectedAttempt, activities, runningProcessDetails, - projectId, }: TaskActivityHistoryProps) { const [expandedOutputs, setExpandedOutputs] = useState>( new Set() @@ -169,7 +167,6 @@ export function TaskActivityHistory({ executionProcess={ runningProcessDetails[activity.execution_process_id] } - projectId={projectId} />
+
+ {!isHeaderCollapsed && ( + + )} + + ); +} + +export default CollapsibleToolbar; diff --git a/frontend/src/components/tasks/DiffCard.tsx b/frontend/src/components/tasks/TaskDetails/DiffCard.tsx similarity index 96% rename from frontend/src/components/tasks/DiffCard.tsx rename to frontend/src/components/tasks/TaskDetails/DiffCard.tsx index 9aa3776e..ae364cc7 100644 --- a/frontend/src/components/tasks/DiffCard.tsx +++ b/frontend/src/components/tasks/TaskDetails/DiffCard.tsx @@ -1,7 +1,8 @@ -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { ChevronDown, ChevronUp, Trash2, GitCompare } from 'lucide-react'; -import type { WorktreeDiff, DiffChunkType, DiffChunk } from 'shared/types'; +import { useCallback, useContext, useState } from 'react'; +import { Button } from '@/components/ui/button.tsx'; +import { ChevronDown, ChevronUp, GitCompare, Trash2 } from 'lucide-react'; +import type { DiffChunk, DiffChunkType, WorktreeDiff } from 'shared/types.ts'; +import { TaskDetailsContext } from '@/components/context/taskDetailsContext.ts'; interface ProcessedLine { content: string; @@ -20,26 +21,31 @@ interface ProcessedSection { interface DiffCardProps { diff: WorktreeDiff | null; - isBackgroundRefreshing?: boolean; - onDeleteFile?: (filePath: string) => void; - deletingFiles?: Set; + deletable?: boolean; compact?: boolean; className?: string; } export function DiffCard({ diff, - isBackgroundRefreshing = false, - onDeleteFile, - deletingFiles = new Set(), + deletable = false, compact = false, className = '', }: DiffCardProps) { + const { deletingFiles, setFileToDelete, isBackgroundRefreshing } = + useContext(TaskDetailsContext); const [collapsedFiles, setCollapsedFiles] = useState>(new Set()); const [expandedSections, setExpandedSections] = useState>( new Set() ); + const onDeleteFile = useCallback( + (filePath: string) => { + setFileToDelete(filePath); + }, + [setFileToDelete] + ); + // Diff processing functions const getChunkClassName = (chunkType: DiffChunkType) => { const baseClass = 'font-mono text-sm whitespace-pre flex w-full'; @@ -361,7 +367,7 @@ export function DiffCard({ )} - {onDeleteFile && ( + {deletable && ( + + + + ); +} + +export default TabNavigation; diff --git a/frontend/src/components/tasks/TaskDetailsHeader.tsx b/frontend/src/components/tasks/TaskDetailsHeader.tsx index 2c66499c..7800efee 100644 --- a/frontend/src/components/tasks/TaskDetailsHeader.tsx +++ b/frontend/src/components/tasks/TaskDetailsHeader.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Edit, Trash2, X, ChevronDown, ChevronUp } from 'lucide-react'; +import { useContext, useState } from 'react'; +import { ChevronDown, ChevronUp, Edit, Trash2, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Chip } from '@/components/ui/chip'; import { @@ -9,9 +9,9 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import type { TaskStatus, TaskWithAttemptStatus } from 'shared/types'; +import { TaskDetailsContext } from '@/components/context/taskDetailsContext.ts'; interface TaskDetailsHeaderProps { - task: TaskWithAttemptStatus; onClose: () => void; onEditTask?: (task: TaskWithAttemptStatus) => void; onDeleteTask?: (taskId: string) => void; @@ -43,11 +43,11 @@ const getTaskStatusDotColor = (status: TaskStatus): string => { }; export function TaskDetailsHeader({ - task, onClose, onEditTask, onDeleteTask, }: TaskDetailsHeaderProps) { + const { task } = useContext(TaskDetailsContext); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); return ( diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 6b9dfa1e..b1c35d8d 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -1,41 +1,22 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; +import { useEffect, useState } from 'react'; import { TaskDetailsHeader } from './TaskDetailsHeader'; -import { TaskDetailsToolbar } from './TaskDetailsToolbar'; -import { NormalizedConversationViewer } from './NormalizedConversationViewer'; import { TaskFollowUpSection } from './TaskFollowUpSection'; import { EditorSelectionDialog } from './EditorSelectionDialog'; -import { useTaskDetails } from '@/hooks/useTaskDetails'; import { - getTaskPanelClasses, getBackdropClasses, + getTaskPanelClasses, } from '@/lib/responsive-config'; -import { makeRequest } from '@/lib/api'; -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { - ChevronDown, - ChevronUp, - MessageSquare, - GitCompare, -} from 'lucide-react'; -import { DiffCard } from './DiffCard'; -import type { - TaskWithAttemptStatus, - EditorType, - Project, - WorktreeDiff, -} from 'shared/types'; +import type { TaskWithAttemptStatus } from 'shared/types'; +import DiffTab from '@/components/tasks/TaskDetails/DiffTab.tsx'; +import LogsTab from '@/components/tasks/TaskDetails/LogsTab.tsx'; +import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmationDialog.tsx'; +import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx'; +import CollapsibleToolbar from '@/components/tasks/TaskDetails/CollapsibleToolbar.tsx'; +import TaskDetailsProvider from '../context/TaskDetailsContextProvider.tsx'; interface TaskDetailsPanelProps { task: TaskWithAttemptStatus | null; - project: Project | null; + projectHasDevScript?: boolean; projectId: string; isOpen: boolean; onClose: () => void; @@ -44,15 +25,9 @@ interface TaskDetailsPanelProps { isDialogOpen?: boolean; } -interface ApiResponse { - success: boolean; - data: T | null; - message: string | null; -} - export function TaskDetailsPanel({ task, - project, + projectHasDevScript, projectId, isOpen, onClose, @@ -61,177 +36,19 @@ export function TaskDetailsPanel({ isDialogOpen = false, }: TaskDetailsPanelProps) { const [showEditorDialog, setShowEditorDialog] = useState(false); - const [shouldAutoScrollLogs, setShouldAutoScrollLogs] = useState(true); - const [conversationUpdateTrigger, setConversationUpdateTrigger] = useState(0); - const scrollContainerRef = useRef(null); - const setupScrollRef = useRef(null); // Tab and collapsible state const [activeTab, setActiveTab] = useState<'logs' | 'diffs'>('logs'); - const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(false); const [userSelectedTab, setUserSelectedTab] = useState(false); - // Diff-related state - const [diff, setDiff] = useState(null); - const [diffLoading, setDiffLoading] = useState(true); - const [diffError, setDiffError] = useState(null); - const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false); - const [deletingFiles, setDeletingFiles] = useState>(new Set()); - const [fileToDelete, setFileToDelete] = useState(null); - - // Use the custom hook for all task details logic - const { - taskAttempts, - selectedAttempt, - attemptData, - loading, - selectedExecutor, - isStopping, - followUpMessage, - isSendingFollowUp, - followUpError, - isStartingDevServer, - devServerDetails, - branches, - selectedBranch, - runningDevServer, - isAttemptRunning, - canSendFollowUp, - processedDevServerLogs, - executionState, - setFollowUpMessage, - setFollowUpError, - setIsHoveringDevServer, - handleAttemptChange, - createNewAttempt, - stopAllExecutions, - startDevServer, - stopDevServer, - openInEditor, - handleSendFollowUp, - } = useTaskDetails(task, projectId, isOpen); - - // Use ref to track loading state to prevent dependency cycles - const diffLoadingRef = useRef(false); - // Reset to logs tab when task changes useEffect(() => { - if (task) { + if (task?.id) { setActiveTab('logs'); setUserSelectedTab(true); // Treat this as a user selection to prevent auto-switching } }, [task?.id]); - // Fetch diff when attempt changes - const fetchDiff = useCallback( - async (isBackgroundRefresh = false) => { - if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) { - setDiff(null); - setDiffLoading(false); - return; - } - - // Prevent multiple concurrent requests - if (diffLoadingRef.current) { - return; - } - - try { - diffLoadingRef.current = true; - if (isBackgroundRefresh) { - setIsBackgroundRefreshing(true); - } else { - setDiffLoading(true); - } - setDiffError(null); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/diff` - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setDiff(result.data); - } else { - setDiffError('Failed to load diff'); - } - } else { - setDiffError('Failed to load diff'); - } - } catch (err) { - setDiffError('Failed to load diff'); - } finally { - diffLoadingRef.current = false; - if (isBackgroundRefresh) { - setIsBackgroundRefreshing(false); - } else { - setDiffLoading(false); - } - } - }, - [projectId, selectedAttempt?.id, selectedAttempt?.task_id] - ); - - useEffect(() => { - if (isOpen) { - fetchDiff(); - } - }, [isOpen, fetchDiff]); - - // Refresh diff when coding agent is running and making changes - useEffect(() => { - if (!executionState || !isOpen || !selectedAttempt) return; - - const isCodingAgentRunning = - executionState.execution_state === 'CodingAgentRunning'; - - if (isCodingAgentRunning) { - // Immediately refresh diff when coding agent starts running - fetchDiff(true); - - // Then refresh diff every 2 seconds while coding agent is active - const interval = setInterval(() => { - fetchDiff(true); - }, 2000); - - return () => { - clearInterval(interval); - }; - } - }, [executionState, isOpen, selectedAttempt, fetchDiff]); - - // Refresh diff when coding agent completes or changes state - useEffect(() => { - if (!executionState || !isOpen || !selectedAttempt) return; - - const isCodingAgentComplete = - executionState.execution_state === 'CodingAgentComplete'; - const isCodingAgentFailed = - executionState.execution_state === 'CodingAgentFailed'; - const isComplete = executionState.execution_state === 'Complete'; - const hasChanges = executionState.has_changes; - - // Fetch diff when coding agent completes, fails, or task is complete and has changes - if ( - (isCodingAgentComplete || isCodingAgentFailed || isComplete) && - hasChanges - ) { - fetchDiff(); - // Auto-switch to diffs tab when changes are detected, but only if user hasn't manually selected a tab - if (activeTab === 'logs' && !userSelectedTab) { - setActiveTab('diffs'); - } - } - }, [ - executionState?.execution_state, - executionState?.has_changes, - isOpen, - selectedAttempt, - fetchDiff, - activeTab, - userSelectedTab, - ]); - // Handle ESC key locally to prevent global navigation useEffect(() => { if (!isOpen || isDialogOpen) return; @@ -248,638 +65,56 @@ export function TaskDetailsPanel({ return () => document.removeEventListener('keydown', handleKeyDown, true); }, [isOpen, onClose, isDialogOpen]); - // Callback to trigger auto-scroll when conversation updates - const handleConversationUpdate = useCallback(() => { - setConversationUpdateTrigger((prev) => prev + 1); - }, []); - - // Auto-scroll to bottom when activities, execution processes, or conversation changes (for logs section) - useEffect(() => { - if ( - shouldAutoScrollLogs && - scrollContainerRef.current && - activeTab === 'logs' - ) { - scrollContainerRef.current.scrollTop = - scrollContainerRef.current.scrollHeight; - } - }, [ - attemptData.activities, - attemptData.processes, - conversationUpdateTrigger, - shouldAutoScrollLogs, - activeTab, - ]); - - // Auto-scroll setup script logs to bottom - useEffect(() => { - if (setupScrollRef.current) { - setupScrollRef.current.scrollTop = setupScrollRef.current.scrollHeight; - } - }, [attemptData.runningProcessDetails]); - - // Handle scroll events to detect manual scrolling (for logs section) - const handleLogsScroll = useCallback(() => { - if (scrollContainerRef.current) { - const { scrollTop, scrollHeight, clientHeight } = - scrollContainerRef.current; - const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5; - - if (isAtBottom && !shouldAutoScrollLogs) { - setShouldAutoScrollLogs(true); - } else if (!isAtBottom && shouldAutoScrollLogs) { - setShouldAutoScrollLogs(false); - } - } - }, [shouldAutoScrollLogs]); - - const handleOpenInEditor = async (editorType?: EditorType) => { - try { - await openInEditor(editorType); - } catch (err) { - if (!editorType) { - setShowEditorDialog(true); - } - } - }; - - const handleDeleteFileClick = (filePath: string) => { - setFileToDelete(filePath); - }; - - const handleConfirmDelete = async () => { - if (!fileToDelete || !projectId || !task?.id || !selectedAttempt?.id) - return; - - try { - setDeletingFiles((prev) => new Set(prev).add(fileToDelete)); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/delete-file?file_path=${encodeURIComponent( - fileToDelete - )}`, - { - method: 'POST', - } - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success) { - fetchDiff(); - } else { - setDiffError(result.message || 'Failed to delete file'); - } - } else { - setDiffError('Failed to delete file'); - } - } catch (err) { - setDiffError('Failed to delete file'); - } finally { - setDeletingFiles((prev) => { - const newSet = new Set(prev); - newSet.delete(fileToDelete); - return newSet; - }); - setFileToDelete(null); - } - }; - - const handleCancelDelete = () => { - setFileToDelete(null); - }; - - // Render tab content based on active tab - const renderTabContent = (): JSX.Element => { - console.log('renderTabContent called with activeTab:', activeTab); - if (activeTab === 'diffs') { - return renderDiffsContent(); - } - return renderLogsContent(); - }; - - // Render diffs content - const renderDiffsContent = (): JSX.Element => { - if (diffLoading) { - return ( -
-
-

Loading changes...

-
- ); - } - - if (diffError) { - return ( -
-

{diffError}

-
- ); - } - - return ( -
- -
- ); - }; - - // Render logs content - const renderLogsContent = (): JSX.Element => { - // Debug logging to help identify the issue - console.log('renderLogsContent called with state:', { - loading, - selectedAttempt: selectedAttempt?.id, - executionState: executionState?.execution_state, - activeTab, - }); - - // Show loading spinner only when we're actually loading data - if (loading) { - return ( -
-
-

Loading...

-
- ); - } - - // If no attempt is selected, show message - if (!selectedAttempt) { - return ( -
- -

No attempt selected

-

Select an attempt to view its logs

-
- ); - } - - // If no execution state, execution hasn't started yet - if (!executionState) { - return ( -
- -

- Task execution not started yet -

-

- Logs will appear here once the task execution begins -

-
- ); - } - - const isSetupRunning = executionState.execution_state === 'SetupRunning'; - const isSetupComplete = executionState.execution_state === 'SetupComplete'; - const isSetupFailed = executionState.execution_state === 'SetupFailed'; - const isCodingAgentRunning = - executionState.execution_state === 'CodingAgentRunning'; - const isCodingAgentComplete = - executionState.execution_state === 'CodingAgentComplete'; - const isCodingAgentFailed = - executionState.execution_state === 'CodingAgentFailed'; - const isComplete = executionState.execution_state === 'Complete'; - const hasChanges = executionState.has_changes; - - // When setup script is running, show setup execution stdio - if (isSetupRunning) { - // Find the setup script process in runningProcessDetails first, then fallback to processes - const setupProcess = executionState.setup_process_id - ? attemptData.runningProcessDetails[executionState.setup_process_id] - : Object.values(attemptData.runningProcessDetails).find( - (process) => process.process_type === 'setupscript' - ); - - return ( -
-
-

Setup Script Running

-

- Preparing the environment for the coding agent... -

-
- - {setupProcess && ( -
- {(() => { - const stdout = setupProcess.stdout || ''; - const stderr = setupProcess.stderr || ''; - const combined = [stdout, stderr].filter(Boolean).join('\n'); - return combined || 'Waiting for setup script output...'; - })()} -
- )} -
- ); - } - - // When setup failed, show error message and conversation - if (isSetupFailed) { - const setupProcess = executionState.setup_process_id - ? attemptData.runningProcessDetails[executionState.setup_process_id] - : Object.values(attemptData.runningProcessDetails).find( - (process) => process.process_type === 'setupscript' - ); - - return ( -
-
-

- Setup Script Failed -

-

- The setup script encountered an error. Error details below: -

-
- - {setupProcess && ( - - )} -
- ); - } - - // When coding agent failed, show error message and conversation - if (isCodingAgentFailed) { - const codingAgentProcess = executionState.coding_agent_process_id - ? attemptData.runningProcessDetails[ - executionState.coding_agent_process_id - ] - : Object.values(attemptData.runningProcessDetails).find( - (process) => process.process_type === 'codingagent' - ); - - return ( -
-
-

- Coding Agent Failed -

-

- The coding agent encountered an error. Error details below: -

-
- - {codingAgentProcess && ( - - )} -
- ); - } - - // When setup is complete but coding agent hasn't started, show waiting state - if ( - isSetupComplete && - !isCodingAgentRunning && - !isCodingAgentComplete && - !isCodingAgentFailed && - !hasChanges - ) { - return ( -
- -

Setup Complete

-

Waiting for coding agent to start...

-
- ); - } - - // When task is complete, show completion message - if (isComplete) { - return ( -
- -

Task Complete

-

- The task has been completed successfully. -

-
- ); - } - - // When coding agent is running or complete, show conversation - if (isCodingAgentRunning || isCodingAgentComplete || hasChanges) { - return ( -
- {loading ? ( -
-
-

Loading...

-
- ) : ( - (() => { - // Find main coding agent process (command: "executor") - let mainCodingAgentProcess = Object.values( - attemptData.runningProcessDetails - ).find( - (process) => - process.process_type === 'codingagent' && - process.command === 'executor' - ); - - if (!mainCodingAgentProcess) { - const mainCodingAgentSummary = attemptData.processes.find( - (process) => - process.process_type === 'codingagent' && - process.command === 'executor' - ); - - if (mainCodingAgentSummary) { - mainCodingAgentProcess = Object.values( - attemptData.runningProcessDetails - ).find((process) => process.id === mainCodingAgentSummary.id); - - if (!mainCodingAgentProcess) { - mainCodingAgentProcess = { - ...mainCodingAgentSummary, - stdout: null, - stderr: null, - } as any; - } - } - } - - // Find follow up executor processes (command: "followup_executor") - const followUpProcesses = attemptData.processes - .filter( - (process) => - process.process_type === 'codingagent' && - process.command === 'followup_executor' - ) - .map((summary) => { - const detailedProcess = Object.values( - attemptData.runningProcessDetails - ).find((process) => process.id === summary.id); - return ( - detailedProcess || - ({ - ...summary, - stdout: null, - stderr: null, - } as any) - ); - }); - - if (mainCodingAgentProcess || followUpProcesses.length > 0) { - return ( -
- {mainCodingAgentProcess && ( -
- -
- )} - {followUpProcesses.map((followUpProcess) => ( -
-
- -
- ))} -
- ); - } - - return ( -
-
-

- Coding Agent Starting -

-

Initializing conversation...

-
- ); - })() - )} -
- ); - } - - // Default case - unexpected state - return ( -
- -

Unknown execution state

-
- ); - }; - - if (!task) return null; - return ( <> - {isOpen && ( - <> + {!task || !isOpen ? null : ( + {/* Backdrop - only on smaller screens (overlay mode) */}
{/* Panel */}
- {/* Header */} - {/* Collapsible Toolbar */} -
-
-

- Task Details -

- -
- {!isHeaderCollapsed && ( - - )} -
+ - {/* Tab Navigation */} -
-
- - -
-
+ {/* Tab Content */}
- {renderTabContent()} + {activeTab === 'diffs' ? : }
- {/* Footer - Follow-up section */} - {selectedAttempt && ( - - )} +
- {/* Editor Selection Dialog */} setShowEditorDialog(false)} - onSelectEditor={handleOpenInEditor} /> - {/* Delete File Confirmation Dialog */} - handleCancelDelete()} - > - - - Delete File - - Are you sure you want to delete the file{' '} - - "{fileToDelete}" - - ? - - -
-
-

- Warning: This action will permanently - remove the entire file from the worktree. This cannot be - undone. -

-
-
- - - - -
-
- + + )} ); diff --git a/frontend/src/components/tasks/TaskDetailsToolbar.tsx b/frontend/src/components/tasks/TaskDetailsToolbar.tsx index 449e2d97..f3fa0260 100644 --- a/frontend/src/components/tasks/TaskDetailsToolbar.tsx +++ b/frontend/src/components/tasks/TaskDetailsToolbar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { ArrowDown, ExternalLink, @@ -50,13 +50,11 @@ import { makeRequest } from '@/lib/api'; import type { BranchStatus, ExecutionProcess, - ExecutionProcessSummary, GitBranch, - Project, TaskAttempt, - TaskWithAttemptStatus, } from 'shared/types'; import { ProvidePatDialog } from '@/components/ProvidePatDialog'; +import { TaskDetailsContext } from '@/components/context/taskDetailsContext.ts'; interface ApiResponse { success: boolean; @@ -65,27 +63,7 @@ interface ApiResponse { } interface TaskDetailsToolbarProps { - task: TaskWithAttemptStatus; - project: Project | null; - projectId: string; - selectedAttempt: TaskAttempt | null; - taskAttempts: TaskAttempt[]; - isAttemptRunning: boolean; - isStopping: boolean; - selectedExecutor: string; - runningDevServer: ExecutionProcessSummary | undefined; - isStartingDevServer: boolean; - devServerDetails: ExecutionProcess | null; - processedDevServerLogs: string; - branches: GitBranch[]; - selectedBranch: string | null; - onAttemptChange: (attemptId: string) => void; - onCreateNewAttempt: (executor?: string, baseBranch?: string) => void; - onStopAllExecutions: () => void; - onStartDevServer: () => void; - onStopDevServer: () => void; - onOpenInEditor: () => void; - onSetIsHoveringDevServer: (hovering: boolean) => void; + projectHasDevScript?: boolean; } const availableExecutors = [ @@ -97,31 +75,35 @@ const availableExecutors = [ ]; export function TaskDetailsToolbar({ - task, - project, - projectId, - selectedAttempt, - taskAttempts, - isAttemptRunning, - isStopping, - selectedExecutor, - runningDevServer, - isStartingDevServer, - devServerDetails, - processedDevServerLogs, - branches, - selectedBranch, - onAttemptChange, - onCreateNewAttempt, - onStopAllExecutions, - onStartDevServer, - onStopDevServer, - onOpenInEditor, - onSetIsHoveringDevServer, + projectHasDevScript, }: TaskDetailsToolbarProps) { + const { + task, + projectId, + setLoading, + setSelectedAttempt, + isStopping, + handleOpenInEditor, + isAttemptRunning, + setAttemptData, + fetchAttemptData, + fetchExecutionState, + selectedAttempt, + setIsStopping, + attemptData, + } = useContext(TaskDetailsContext); + const [taskAttempts, setTaskAttempts] = useState([]); + const { config } = useConfig(); const [branchSearchTerm, setBranchSearchTerm] = useState(''); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState(null); + + const [selectedExecutor, setSelectedExecutor] = useState( + config?.executor.type || 'claude' + ); + // State for create attempt mode const [isInCreateAttemptMode, setIsInCreateAttemptMode] = useState(false); const [createAttemptBranch, setCreateAttemptBranch] = useState( @@ -146,6 +128,88 @@ export function TaskDetailsToolbar({ const [showPatDialog, setShowPatDialog] = useState(false); const [patDialogError, setPatDialogError] = useState(null); + const [devServerDetails, setDevServerDetails] = + useState(null); + const [isHoveringDevServer, setIsHoveringDevServer] = useState(false); + + // Find running dev server in current project + const runningDevServer = useMemo(() => { + return attemptData.processes.find( + (process) => + process.process_type === 'devserver' && process.status === 'running' + ); + }, [attemptData.processes]); + + const fetchDevServerDetails = useCallback(async () => { + if (!runningDevServer || !task || !selectedAttempt) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/execution-processes/${runningDevServer.id}` + ); + if (response.ok) { + const result: ApiResponse = await response.json(); + if (result.success && result.data) { + setDevServerDetails(result.data); + } + } + } catch (err) { + console.error('Failed to fetch dev server details:', err); + } + }, [runningDevServer, task, selectedAttempt, projectId]); + + useEffect(() => { + if (!isHoveringDevServer || !runningDevServer) { + setDevServerDetails(null); + return; + } + + fetchDevServerDetails(); + const interval = setInterval(fetchDevServerDetails, 2000); + return () => clearInterval(interval); + }, [isHoveringDevServer, runningDevServer, fetchDevServerDetails]); + + const processedDevServerLogs = useMemo(() => { + if (!devServerDetails) return 'No output yet...'; + + const stdout = devServerDetails.stdout || ''; + const stderr = devServerDetails.stderr || ''; + const allOutput = stdout + (stderr ? '\n' + stderr : ''); + const lines = allOutput.split('\n').filter((line) => line.trim()); + const lastLines = lines.slice(-10); + return lastLines.length > 0 ? lastLines.join('\n') : 'No output yet...'; + }, [devServerDetails]); + + const fetchProjectBranches = useCallback(async () => { + try { + const response = await makeRequest(`/api/projects/${projectId}/branches`); + if (response.ok) { + const result: ApiResponse = await response.json(); + if (result.success && result.data) { + setBranches(result.data); + // Set current branch as default + const currentBranch = result.data.find((b) => b.is_current); + if (currentBranch && !selectedBranch) { + setSelectedBranch(currentBranch.name); + } + } + } + } catch (err) { + console.error('Failed to fetch project branches:', err); + } + }, [projectId, selectedBranch]); + + useEffect(() => { + fetchProjectBranches(); + }, [fetchProjectBranches]); + + // Set default executor from config + useEffect(() => { + if (config && config.executor.type !== selectedExecutor) { + setSelectedExecutor(config.executor.type); + } + }, [config, selectedExecutor]); + // Set create attempt mode when there are no attempts useEffect(() => { setIsInCreateAttemptMode(taskAttempts.length === 0); @@ -185,6 +249,165 @@ export function TaskDetailsToolbar({ } }, [selectedAttempt?.base_branch]); + const onCreateNewAttempt = async (executor?: string, baseBranch?: string) => { + if (!task) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts`, + { + method: 'POST', + body: JSON.stringify({ + executor: executor || selectedExecutor, + base_branch: baseBranch || selectedBranch, + }), + } + ); + + if (response.ok) { + fetchTaskAttempts(); + } + } catch (err) { + console.error('Failed to create new attempt:', err); + } + }; + + const fetchTaskAttempts = useCallback(async () => { + if (!task) return; + + try { + setLoading(true); + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts` + ); + + if (response.ok) { + const result: ApiResponse = await response.json(); + if (result.success && result.data) { + setTaskAttempts(result.data); + + if (result.data.length > 0) { + const latestAttempt = result.data.reduce((latest, current) => + new Date(current.created_at) > new Date(latest.created_at) + ? current + : latest + ); + setSelectedAttempt(latestAttempt); + fetchAttemptData(latestAttempt.id, latestAttempt.task_id); + fetchExecutionState(latestAttempt.id, latestAttempt.task_id); + } else { + setSelectedAttempt(null); + setAttemptData({ + activities: [], + processes: [], + runningProcessDetails: {}, + }); + } + } + } + } catch (err) { + console.error('Failed to fetch task attempts:', err); + } finally { + setLoading(false); + } + }, [task, projectId, fetchAttemptData, fetchExecutionState]); + + useEffect(() => { + fetchTaskAttempts(); + }, [fetchTaskAttempts]); + + const [isStartingDevServer, setIsStartingDevServer] = useState(false); + + const startDevServer = async () => { + if (!task || !selectedAttempt) return; + + setIsStartingDevServer(true); + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/start-dev-server`, + { + method: 'POST', + } + ); + + if (!response.ok) { + throw new Error('Failed to start dev server'); + } + + const data: ApiResponse = await response.json(); + + if (!data.success) { + throw new Error(data.message || 'Failed to start dev server'); + } + + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + } catch (err) { + console.error('Failed to start dev server:', err); + } finally { + setIsStartingDevServer(false); + } + }; + + const stopDevServer = async () => { + if (!task || !selectedAttempt || !runningDevServer) return; + + setIsStartingDevServer(true); + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/execution-processes/${runningDevServer.id}/stop`, + { + method: 'POST', + } + ); + + if (!response.ok) { + throw new Error('Failed to stop dev server'); + } + + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + } catch (err) { + console.error('Failed to stop dev server:', err); + } finally { + setIsStartingDevServer(false); + } + }; + + const stopAllExecutions = async () => { + if (!task || !selectedAttempt) return; + + try { + setIsStopping(true); + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/stop`, + { + method: 'POST', + } + ); + + if (response.ok) { + await fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + setTimeout(() => { + fetchAttemptData(selectedAttempt.id, selectedAttempt.task_id); + }, 1000); + } + } catch (err) { + console.error('Failed to stop executions:', err); + } finally { + setIsStopping(false); + } + }; + + const handleAttemptChange = useCallback( + (attempt: TaskAttempt) => { + setSelectedAttempt(attempt); + fetchAttemptData(attempt.id, attempt.task_id); + fetchExecutionState(attempt.id, attempt.task_id); + }, + [fetchAttemptData, fetchExecutionState, setSelectedAttempt] + ); + // Branch status fetching const fetchBranchStatus = useCallback(async () => { if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return; @@ -722,7 +945,7 @@ export function TaskDetailsToolbar({ + size="sm" + > + {isSendingFollowUp ? ( +
+ ) : ( + <> + + Send + + )} + +
- + ) ); } diff --git a/frontend/src/components/tasks/TaskKanbanBoard.tsx b/frontend/src/components/tasks/TaskKanbanBoard.tsx index 353614b5..45a3fde3 100644 --- a/frontend/src/components/tasks/TaskKanbanBoard.tsx +++ b/frontend/src/components/tasks/TaskKanbanBoard.tsx @@ -1,9 +1,10 @@ +import { useMemo } from 'react'; import { - KanbanProvider, - KanbanBoard, - KanbanHeader, - KanbanCards, type DragEndEvent, + KanbanBoard, + KanbanCards, + KanbanHeader, + KanbanProvider, } from '@/components/ui/shadcn-io/kanban'; import { TaskCard } from './TaskCard'; import type { TaskStatus, TaskWithAttemptStatus } from 'shared/types'; @@ -51,46 +52,39 @@ export function TaskKanbanBoard({ onDeleteTask, onViewTaskDetails, }: TaskKanbanBoardProps) { - const filterTasks = (tasks: Task[]) => { + // Memoize filtered tasks + const filteredTasks = useMemo(() => { if (!searchQuery.trim()) { return tasks; } - const query = searchQuery.toLowerCase(); return tasks.filter( (task) => task.title.toLowerCase().includes(query) || (task.description && task.description.toLowerCase().includes(query)) ); - }; + }, [tasks, searchQuery]); - const groupTasksByStatus = () => { + // Memoize grouped tasks + const groupedTasks = useMemo(() => { const groups: Record = {} as Record; - - // Initialize groups for all possible statuses allTaskStatuses.forEach((status) => { groups[status] = []; }); - - const filteredTasks = filterTasks(tasks); - filteredTasks.forEach((task) => { - // Convert old capitalized status to lowercase if needed const normalizedStatus = task.status.toLowerCase() as TaskStatus; if (groups[normalizedStatus]) { groups[normalizedStatus].push(task); } else { - // Default to todo if status doesn't match any expected value groups['todo'].push(task); } }); - return groups; - }; + }, [filteredTasks]); return ( - {Object.entries(groupTasksByStatus()).map(([status, statusTasks]) => ( + {Object.entries(groupedTasks).map(([status, statusTasks]) => ( ([]); - const [selectedAttempt, setSelectedAttempt] = useState( - null - ); - const [attemptData, setAttemptData] = useState<{ - activities: TaskAttemptActivityWithPrompt[]; - processes: ExecutionProcessSummary[]; - runningProcessDetails: Record; - }>({ - activities: [], - processes: [], - runningProcessDetails: {}, - }); - const [loading, setLoading] = useState(false); - const [selectedExecutor, setSelectedExecutor] = useState( - config?.executor.type || 'claude' - ); - const [isStopping, setIsStopping] = useState(false); - const [followUpMessage, setFollowUpMessage] = useState(''); - const [isSendingFollowUp, setIsSendingFollowUp] = useState(false); - const [followUpError, setFollowUpError] = useState(null); - const [isStartingDevServer, setIsStartingDevServer] = useState(false); - const [devServerDetails, setDevServerDetails] = - useState(null); - const [isHoveringDevServer, setIsHoveringDevServer] = useState(false); - const [branches, setBranches] = useState([]); - const [selectedBranch, setSelectedBranch] = useState(null); - const [executionState, setExecutionState] = useState( - null - ); - - // Find running dev server in current project - const runningDevServer = useMemo(() => { - return attemptData.processes.find( - (process) => - process.process_type === 'devserver' && process.status === 'running' - ); - }, [attemptData.processes]); - - // Check if any execution process is currently running - const isAttemptRunning = useMemo(() => { - if (!selectedAttempt || isStopping) { - return false; - } - - return attemptData.processes.some( - (process) => - (process.process_type === 'codingagent' || - process.process_type === 'setupscript') && - process.status === 'running' - ); - }, [selectedAttempt, attemptData.processes, isStopping]); - - // Check if follow-up should be enabled - const canSendFollowUp = useMemo(() => { - if ( - !selectedAttempt || - attemptData.activities.length === 0 || - isAttemptRunning || - isSendingFollowUp - ) { - return false; - } - - const codingAgentActivities = attemptData.activities.filter( - (activity) => activity.status === 'executorcomplete' - ); - - return codingAgentActivities.length > 0; - }, [ - selectedAttempt, - attemptData.activities, - isAttemptRunning, - isSendingFollowUp, - ]); - - // Memoize processed dev server logs - const processedDevServerLogs = useMemo(() => { - if (!devServerDetails) return 'No output yet...'; - - const stdout = devServerDetails.stdout || ''; - const stderr = devServerDetails.stderr || ''; - const allOutput = stdout + (stderr ? '\n' + stderr : ''); - const lines = allOutput.split('\n').filter((line) => line.trim()); - const lastLines = lines.slice(-10); - return lastLines.length > 0 ? lastLines.join('\n') : 'No output yet...'; - }, [devServerDetails]); - - // Define callbacks first - const fetchAttemptData = useCallback( - async (attemptId: string) => { - if (!task) return; - - // Find the attempt to get the task_id - const attempt = taskAttempts.find((a) => a.id === attemptId); - const taskId = attempt?.task_id || task.id; - - try { - const [activitiesResponse, processesResponse] = await Promise.all([ - makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/activities` - ), - makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/execution-processes` - ), - ]); - - if (activitiesResponse.ok && processesResponse.ok) { - const activitiesResult: ApiResponse = - await activitiesResponse.json(); - const processesResult: ApiResponse = - await processesResponse.json(); - - if ( - activitiesResult.success && - processesResult.success && - activitiesResult.data && - processesResult.data - ) { - const runningActivities = activitiesResult.data.filter( - (activity) => - activity.status === 'setuprunning' || - activity.status === 'executorrunning' - ); - - const runningProcessDetails: Record = {}; - - // Fetch details for running activities - for (const activity of runningActivities) { - try { - const detailResponse = await makeRequest( - `/api/projects/${projectId}/execution-processes/${activity.execution_process_id}` - ); - if (detailResponse.ok) { - const detailResult: ApiResponse = - await detailResponse.json(); - if (detailResult.success && detailResult.data) { - runningProcessDetails[activity.execution_process_id] = - detailResult.data; - } - } - } catch (err) { - console.error( - `Failed to fetch execution process ${activity.execution_process_id}:`, - err - ); - } - } - - // Also fetch setup script process details if it exists in the processes - const setupProcess = processesResult.data.find( - (process) => process.process_type === 'setupscript' - ); - if (setupProcess && !runningProcessDetails[setupProcess.id]) { - try { - const detailResponse = await makeRequest( - `/api/projects/${projectId}/execution-processes/${setupProcess.id}` - ); - if (detailResponse.ok) { - const detailResult: ApiResponse = - await detailResponse.json(); - if (detailResult.success && detailResult.data) { - runningProcessDetails[setupProcess.id] = detailResult.data; - } - } - } catch (err) { - console.error( - `Failed to fetch setup process details ${setupProcess.id}:`, - err - ); - } - } - - setAttemptData({ - activities: activitiesResult.data, - processes: processesResult.data, - runningProcessDetails, - }); - } - } - } catch (err) { - console.error('Failed to fetch attempt data:', err); - } - }, - [task, projectId] - ); - - const fetchExecutionState = useCallback( - async (attemptId: string) => { - if (!task) return; - - // Find the attempt to get the task_id - const attempt = taskAttempts.find((a) => a.id === attemptId); - const taskId = attempt?.task_id || task.id; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}` - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setExecutionState(result.data); - } - } - } catch (err) { - console.error('Failed to fetch execution state:', err); - } - }, - [task, projectId] - ); - - const fetchTaskAttempts = useCallback(async () => { - if (!task) return; - - try { - setLoading(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts` - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setTaskAttempts(result.data); - - if (result.data.length > 0) { - const latestAttempt = result.data.reduce((latest, current) => - new Date(current.created_at) > new Date(latest.created_at) - ? current - : latest - ); - setSelectedAttempt(latestAttempt); - fetchAttemptData(latestAttempt.id); - fetchExecutionState(latestAttempt.id); - } else { - setSelectedAttempt(null); - setAttemptData({ - activities: [], - processes: [], - runningProcessDetails: {}, - }); - } - } - } - } catch (err) { - console.error('Failed to fetch task attempts:', err); - } finally { - setLoading(false); - } - }, [task, projectId, fetchAttemptData, fetchExecutionState]); - - // Fetch dev server details when hovering - const fetchDevServerDetails = useCallback(async () => { - if (!runningDevServer || !task || !selectedAttempt) return; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/execution-processes/${runningDevServer.id}` - ); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setDevServerDetails(result.data); - } - } - } catch (err) { - console.error('Failed to fetch dev server details:', err); - } - }, [runningDevServer, task, selectedAttempt, projectId]); - - // Fetch project branches - const fetchProjectBranches = useCallback(async () => { - try { - const response = await makeRequest(`/api/projects/${projectId}/branches`); - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setBranches(result.data); - // Set current branch as default - const currentBranch = result.data.find((b) => b.is_current); - if (currentBranch && !selectedBranch) { - setSelectedBranch(currentBranch.name); - } - } - } - } catch (err) { - console.error('Failed to fetch project branches:', err); - } - }, [projectId, selectedBranch]); - - // Set default executor from config - useEffect(() => { - if (config && config.executor.type !== selectedExecutor) { - setSelectedExecutor(config.executor.type); - } - }, [config, selectedExecutor]); - - useEffect(() => { - if (task && isOpen) { - fetchTaskAttempts(); - fetchProjectBranches(); - } - }, [task, isOpen, fetchTaskAttempts, fetchProjectBranches]); - - // Load attempt data when selectedAttempt changes - useEffect(() => { - if (selectedAttempt && task) { - fetchAttemptData(selectedAttempt.id); - fetchExecutionState(selectedAttempt.id); - } - }, [selectedAttempt, task, fetchAttemptData, fetchExecutionState]); - - // Polling for updates when attempt is running - useEffect(() => { - if (!isAttemptRunning || !task) return; - - const interval = setInterval(() => { - if (selectedAttempt) { - fetchAttemptData(selectedAttempt.id); - fetchExecutionState(selectedAttempt.id); - } - }, 2000); - - return () => clearInterval(interval); - }, [ - isAttemptRunning, - task, - selectedAttempt, - fetchAttemptData, - fetchExecutionState, - ]); - - // Poll dev server details while hovering - useEffect(() => { - if (!isHoveringDevServer || !runningDevServer) { - setDevServerDetails(null); - return; - } - - fetchDevServerDetails(); - const interval = setInterval(fetchDevServerDetails, 2000); - return () => clearInterval(interval); - }, [isHoveringDevServer, runningDevServer, fetchDevServerDetails]); - - const handleAttemptChange = (attemptId: string) => { - const attempt = taskAttempts.find((a) => a.id === attemptId); - if (attempt) { - setSelectedAttempt(attempt); - fetchAttemptData(attempt.id); - fetchExecutionState(attempt.id); - } - }; - - const createNewAttempt = async (executor?: string, baseBranch?: string) => { - if (!task) return; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - executor: executor || selectedExecutor, - base_branch: baseBranch || selectedBranch, - }), - } - ); - - if (response.ok) { - fetchTaskAttempts(); - } - } catch (err) { - console.error('Failed to create new attempt:', err); - } - }; - - const stopAllExecutions = async () => { - if (!task || !selectedAttempt) return; - - try { - setIsStopping(true); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/stop`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - if (response.ok) { - await fetchAttemptData(selectedAttempt.id); - setTimeout(() => { - fetchAttemptData(selectedAttempt.id); - }, 1000); - } - } catch (err) { - console.error('Failed to stop executions:', err); - } finally { - setIsStopping(false); - } - }; - - const startDevServer = async () => { - if (!task || !selectedAttempt) return; - - setIsStartingDevServer(true); - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/start-dev-server`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - if (!response.ok) { - throw new Error('Failed to start dev server'); - } - - const data: ApiResponse = await response.json(); - - if (!data.success) { - throw new Error(data.message || 'Failed to start dev server'); - } - - fetchAttemptData(selectedAttempt.id); - } catch (err) { - console.error('Failed to start dev server:', err); - } finally { - setIsStartingDevServer(false); - } - }; - - const stopDevServer = async () => { - if (!task || !selectedAttempt || !runningDevServer) return; - - setIsStartingDevServer(true); - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/execution-processes/${runningDevServer.id}/stop`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - } - ); - - if (!response.ok) { - throw new Error('Failed to stop dev server'); - } - - fetchAttemptData(selectedAttempt.id); - } catch (err) { - console.error('Failed to stop dev server:', err); - } finally { - setIsStartingDevServer(false); - } - }; - - const openInEditor = async (editorType?: EditorType) => { - if (!task || !selectedAttempt) return; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/open-editor`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(editorType ? { editor_type: editorType } : null), - } - ); - - if (!response.ok) { - throw new Error('Failed to open editor'); - } - } catch (err) { - console.error('Failed to open editor:', err); - throw err; - } - }; - - const handleSendFollowUp = async () => { - if (!task || !selectedAttempt || !followUpMessage.trim()) return; - - try { - setIsSendingFollowUp(true); - setFollowUpError(null); - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${selectedAttempt.task_id}/attempts/${selectedAttempt.id}/follow-up`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - prompt: followUpMessage.trim(), - }), - } - ); - - if (response.ok) { - setFollowUpMessage(''); - fetchAttemptData(selectedAttempt.id); - } else { - const errorText = await response.text(); - setFollowUpError( - `Failed to start follow-up execution: ${ - errorText || response.statusText - }` - ); - } - } catch (err) { - setFollowUpError( - `Failed to send follow-up: ${ - err instanceof Error ? err.message : 'Unknown error' - }` - ); - } finally { - setIsSendingFollowUp(false); - } - }; - - return { - // State - taskAttempts, - selectedAttempt, - attemptData, - loading, - selectedExecutor, - isStopping, - followUpMessage, - isSendingFollowUp, - followUpError, - isStartingDevServer, - devServerDetails, - isHoveringDevServer, - branches, - selectedBranch, - executionState, - - // Computed - runningDevServer, - isAttemptRunning, - canSendFollowUp, - processedDevServerLogs, - - // Actions - setSelectedExecutor, - setFollowUpMessage, - setFollowUpError, - setIsHoveringDevServer, - setSelectedBranch, - handleAttemptChange, - createNewAttempt, - stopAllExecutions, - startDevServer, - stopDevServer, - openInEditor, - handleSendFollowUp, - }; -} diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index df6a62be..c7e95312 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -1,26 +1,26 @@ -import { useState, useEffect, useCallback } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; -import { Plus, Settings, FolderOpen } from 'lucide-react'; +import { FolderOpen, Plus, Settings } from 'lucide-react'; import { makeRequest } from '@/lib/api'; import { TaskFormDialog } from '@/components/tasks/TaskFormDialog'; import { ProjectForm } from '@/components/projects/project-form'; import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts'; import { - getMainContainerClasses, getKanbanSectionClasses, + getMainContainerClasses, } from '@/lib/responsive-config'; import { TaskKanbanBoard } from '@/components/tasks/TaskKanbanBoard'; import { TaskDetailsPanel } from '@/components/tasks/TaskDetailsPanel'; import type { + CreateTaskAndStart, + ExecutorConfig, + ProjectWithBranch, TaskStatus, TaskWithAttemptStatus, - ProjectWithBranch, - ExecutorConfig, - CreateTaskAndStart, } from 'shared/types'; import type { DragEndEvent } from '@/components/ui/shadcn-io/kanban'; @@ -459,7 +459,7 @@ export function ProjectTasks() { {isPanelOpen && ( = { success: boolean, data: T | null, message: string | null, }; -export type Config = { theme: ThemeMode, executor: ExecutorConfig, disclaimer_acknowledged: boolean, onboarding_acknowledged: boolean, sound_alerts: boolean, sound_file: SoundFile, push_notifications: boolean, editor: EditorConfig, github: GitHubConfig, analytics_enabled: boolean | null, }; +export type Config = { + theme: ThemeMode, + executor: ExecutorConfig, + disclaimer_acknowledged: boolean, + onboarding_acknowledged: boolean, + sound_alerts: boolean, + sound_file: SoundFile, + push_notifications: boolean, + editor: EditorConfig, + github: GitHubConfig, + analytics_enabled: boolean | null, +}; export type ThemeMode = "light" | "dark" | "system" | "purple" | "green" | "blue" | "orange" | "red"; export type EditorConfig = { editor_type: EditorType, custom_command: string | null, }; -export type GitHubConfig = { pat: string | null, token: string | null, username: string | null, primary_email: string | null, default_pr_base: string | null, }; +export type GitHubConfig = { + pat: string | null, + token: string | null, + username: string | null, + primary_email: string | null, + default_pr_base: string | null, +}; export type EditorType = "vscode" | "cursor" | "windsurf" | "intellij" | "zed" | "custom"; export type EditorConstants = { editor_types: Array, editor_labels: Array, }; -export type SoundFile = "abstract-sound1" | "abstract-sound2" | "abstract-sound3" | "abstract-sound4" | "cow-mooing" | "phone-vibration" | "rooster"; +export type SoundFile = + "abstract-sound1" + | "abstract-sound2" + | "abstract-sound3" + | "abstract-sound4" + | "cow-mooing" + | "phone-vibration" + | "rooster"; export type SoundConstants = { sound_files: Array, sound_labels: Array, }; export type ConfigConstants = { editor: EditorConstants, sound: SoundConstants, }; -export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { "type": "opencode" }; +export type ExecutorConfig = { "type": "echo" } | { "type": "claude" } | { "type": "amp" } | { "type": "gemini" } | { + "type": "opencode" +}; export type ExecutorConstants = { executor_types: Array, executor_labels: Array, }; -export type CreateProject = { name: string, git_repo_path: string, use_existing_repo: boolean, setup_script: string | null, dev_script: string | null, }; +export type CreateProject = { + name: string, + git_repo_path: string, + use_existing_repo: boolean, + setup_script: string | null, + dev_script: string | null, +}; -export type Project = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, created_at: Date, updated_at: Date, }; +export type Project = { + id: string, + name: string, + git_repo_path: string, + setup_script: string | null, + dev_script: string | null, + created_at: Date, + updated_at: Date, +}; -export type ProjectWithBranch = { id: string, name: string, git_repo_path: string, setup_script: string | null, dev_script: string | null, current_branch: string | null, created_at: Date, updated_at: Date, }; +export type ProjectWithBranch = { + id: string, + name: string, + git_repo_path: string, + setup_script: string | null, + dev_script: string | null, + current_branch: string | null, + created_at: Date, + updated_at: Date, +}; -export type UpdateProject = { name: string | null, git_repo_path: string | null, setup_script: string | null, dev_script: string | null, }; +export type UpdateProject = { + name: string | null, + git_repo_path: string | null, + setup_script: string | null, + dev_script: string | null, +}; export type SearchResult = { path: string, is_file: boolean, match_type: SearchMatchType, }; @@ -44,19 +98,64 @@ export type CreateBranch = { name: string, base_branch: string | null, }; export type CreateTask = { project_id: string, title: string, description: string | null, }; -export type CreateTaskAndStart = { project_id: string, title: string, description: string | null, executor: ExecutorConfig | null, }; +export type CreateTaskAndStart = { + project_id: string, + title: string, + description: string | null, + executor: ExecutorConfig | null, +}; export type TaskStatus = "todo" | "inprogress" | "inreview" | "done" | "cancelled"; -export type Task = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, }; +export type Task = { + id: string, + project_id: string, + title: string, + description: string | null, + status: TaskStatus, + created_at: string, + updated_at: string, +}; -export type TaskWithAttemptStatus = { id: string, project_id: string, title: string, description: string | null, status: TaskStatus, created_at: string, updated_at: string, has_in_progress_attempt: boolean, has_merged_attempt: boolean, has_failed_attempt: boolean, }; +export type TaskWithAttemptStatus = { + id: string, + project_id: string, + title: string, + description: string | null, + status: TaskStatus, + created_at: string, + updated_at: string, + has_in_progress_attempt: boolean, + has_merged_attempt: boolean, + has_failed_attempt: boolean, +}; export type UpdateTask = { title: string | null, description: string | null, status: TaskStatus | null, }; -export type TaskAttemptStatus = "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed"; +export type TaskAttemptStatus = + "setuprunning" + | "setupcomplete" + | "setupfailed" + | "executorrunning" + | "executorcomplete" + | "executorfailed"; -export type TaskAttempt = { id: string, task_id: string, worktree_path: string, branch: string, base_branch: string, merge_commit: string | null, executor: string | null, pr_url: string | null, pr_number: bigint | null, pr_status: string | null, pr_merged_at: string | null, worktree_deleted: boolean, created_at: string, updated_at: string, }; +export type TaskAttempt = { + id: string, + task_id: string, + worktree_path: string, + branch: string, + base_branch: string, + merge_commit: string | null, + executor: string | null, + pr_url: string | null, + pr_number: bigint | null, + pr_status: string | null, + pr_merged_at: string | null, + worktree_deleted: boolean, + created_at: string, + updated_at: string, +}; export type CreateTaskAttempt = { executor: string | null, base_branch: string | null, }; @@ -64,11 +163,34 @@ export type UpdateTaskAttempt = Record; export type CreateFollowUpAttempt = { prompt: string, }; -export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, }; +export type TaskAttemptActivity = { + id: string, + execution_process_id: string, + status: TaskAttemptStatus, + note: string | null, + created_at: string, +}; -export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, }; +export type TaskAttemptActivityWithPrompt = { + id: string, + execution_process_id: string, + status: TaskAttemptStatus, + note: string | null, + created_at: string, + prompt: string | null, +}; -export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, }; +export type AttemptData = { + activities: TaskAttemptActivityWithPrompt[]; + processes: ExecutionProcessSummary[]; + runningProcessDetails: Record; +} + +export type CreateTaskAttemptActivity = { + execution_process_id: string, + status: TaskAttemptStatus | null, + note: string | null, +}; export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, }; @@ -80,37 +202,125 @@ export type FileDiff = { path: string, chunks: Array, }; export type WorktreeDiff = { files: Array, }; -export type BranchStatus = { is_behind: boolean, commits_behind: number, commits_ahead: number, up_to_date: boolean, merged: boolean, has_uncommitted_changes: boolean, base_branch_name: string, }; +export type BranchStatus = { + is_behind: boolean, + commits_behind: number, + commits_ahead: number, + up_to_date: boolean, + merged: boolean, + has_uncommitted_changes: boolean, + base_branch_name: string, +}; -export type ExecutionState = "NotStarted" | "SetupRunning" | "SetupComplete" | "SetupFailed" | "CodingAgentRunning" | "CodingAgentComplete" | "CodingAgentFailed" | "Complete"; +export type ExecutionState = + "NotStarted" + | "SetupRunning" + | "SetupComplete" + | "SetupFailed" + | "CodingAgentRunning" + | "CodingAgentComplete" + | "CodingAgentFailed" + | "Complete"; -export type TaskAttemptState = { execution_state: ExecutionState, has_changes: boolean, has_setup_script: boolean, setup_process_id: string | null, coding_agent_process_id: string | null, }; +export type TaskAttemptState = { + execution_state: ExecutionState, + has_changes: boolean, + has_setup_script: boolean, + setup_process_id: string | null, + coding_agent_process_id: string | null, +}; -export type ExecutionProcess = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, stdout: string | null, stderr: string | null, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, }; +export type ExecutionProcess = { + id: string, + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + status: ExecutionProcessStatus, + command: string, + args: string | null, + working_directory: string, + stdout: string | null, + stderr: string | null, + exit_code: bigint | null, + started_at: string, + completed_at: string | null, + created_at: string, + updated_at: string, +}; -export type ExecutionProcessSummary = { id: string, task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, status: ExecutionProcessStatus, command: string, args: string | null, working_directory: string, exit_code: bigint | null, started_at: string, completed_at: string | null, created_at: string, updated_at: string, }; +export type ExecutionProcessSummary = { + id: string, + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + status: ExecutionProcessStatus, + command: string, + args: string | null, + working_directory: string, + exit_code: bigint | null, + started_at: string, + completed_at: string | null, + created_at: string, + updated_at: string, +}; export type ExecutionProcessStatus = "running" | "completed" | "failed" | "killed"; export type ExecutionProcessType = "setupscript" | "codingagent" | "devserver"; -export type CreateExecutionProcess = { task_attempt_id: string, process_type: ExecutionProcessType, executor_type: string | null, command: string, args: string | null, working_directory: string, }; +export type CreateExecutionProcess = { + task_attempt_id: string, + process_type: ExecutionProcessType, + executor_type: string | null, + command: string, + args: string | null, + working_directory: string, +}; -export type UpdateExecutionProcess = { status: ExecutionProcessStatus | null, exit_code: bigint | null, completed_at: string | null, }; +export type UpdateExecutionProcess = { + status: ExecutionProcessStatus | null, + exit_code: bigint | null, + completed_at: string | null, +}; -export type ExecutorSession = { id: string, task_attempt_id: string, execution_process_id: string, session_id: string | null, prompt: string | null, summary: string | null, created_at: string, updated_at: string, }; +export type ExecutorSession = { + id: string, + task_attempt_id: string, + execution_process_id: string, + session_id: string | null, + prompt: string | null, + summary: string | null, + created_at: string, + updated_at: string, +}; export type CreateExecutorSession = { task_attempt_id: string, execution_process_id: string, prompt: string | null, }; export type UpdateExecutorSession = { session_id: string | null, prompt: string | null, summary: string | null, }; -export type NormalizedConversation = { entries: Array, session_id: string | null, executor_type: string, prompt: string | null, summary: string | null, }; +export type NormalizedConversation = { + entries: Array, + session_id: string | null, + executor_type: string, + prompt: string | null, + summary: string | null, +}; export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, }; -export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, } | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" }; +export type NormalizedEntryType = { "type": "user_message" } | { "type": "assistant_message" } | { + "type": "tool_use", + tool_name: string, + action_type: ActionType, +} | { "type": "system_message" } | { "type": "error_message" } | { "type": "thinking" }; -export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { "action": "command_run", command: string, } | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { "action": "task_create", description: string, } | { "action": "other", description: string, }; +export type ActionType = { "action": "file_read", path: string, } | { "action": "file_write", path: string, } | { + "action": "command_run", + command: string, +} | { "action": "search", query: string, } | { "action": "web_fetch", url: string, } | { + "action": "task_create", + description: string, +} | { "action": "other", description: string, }; // Generated constants export const EXECUTOR_TYPES: string[] = [ @@ -123,7 +333,7 @@ export const EXECUTOR_TYPES: string[] = [ export const EDITOR_TYPES: EditorType[] = [ "vscode", - "cursor", + "cursor", "windsurf", "intellij", "zed",