From e4188ed949b0710e15650efd808ccd0c35e88821 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Thu, 10 Jul 2025 14:05:57 +0100 Subject: [PATCH] feat: improve sidebar layout (#112) * improve diff box styling * separate logs and diffs tabs * improve sidebar layout * fix tsc errors --- frontend/src/components/tasks/DiffCard.tsx | 476 ++++++++ .../tasks/NormalizedConversationViewer.tsx | 271 ++++- .../src/components/tasks/TaskDetailsPanel.tsx | 1071 +++++------------ 3 files changed, 992 insertions(+), 826 deletions(-) create mode 100644 frontend/src/components/tasks/DiffCard.tsx diff --git a/frontend/src/components/tasks/DiffCard.tsx b/frontend/src/components/tasks/DiffCard.tsx new file mode 100644 index 00000000..9aa3776e --- /dev/null +++ b/frontend/src/components/tasks/DiffCard.tsx @@ -0,0 +1,476 @@ +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'; + +interface ProcessedLine { + content: string; + chunkType: DiffChunkType; + oldLineNumber?: number; + newLineNumber?: number; +} + +interface ProcessedSection { + type: 'context' | 'change' | 'expanded'; + lines: ProcessedLine[]; + expandKey?: string; + expandedAbove?: boolean; + expandedBelow?: boolean; +} + +interface DiffCardProps { + diff: WorktreeDiff | null; + isBackgroundRefreshing?: boolean; + onDeleteFile?: (filePath: string) => void; + deletingFiles?: Set; + compact?: boolean; + className?: string; +} + +export function DiffCard({ + diff, + isBackgroundRefreshing = false, + onDeleteFile, + deletingFiles = new Set(), + compact = false, + className = '', +}: DiffCardProps) { + const [collapsedFiles, setCollapsedFiles] = useState>(new Set()); + const [expandedSections, setExpandedSections] = useState>( + new Set() + ); + + // Diff processing functions + const getChunkClassName = (chunkType: DiffChunkType) => { + const baseClass = 'font-mono text-sm whitespace-pre flex w-full'; + + switch (chunkType) { + case 'Insert': + return `${baseClass} bg-green-50 dark:bg-green-900/20 text-green-900 dark:text-green-100`; + case 'Delete': + return `${baseClass} bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-100`; + case 'Equal': + default: + return `${baseClass} text-muted-foreground`; + } + }; + + const getLineNumberClassName = (chunkType: DiffChunkType) => { + const baseClass = + 'flex-shrink-0 w-12 px-1.5 text-xs border-r select-none min-h-[1.25rem] flex items-center'; + + switch (chunkType) { + case 'Insert': + return `${baseClass} text-green-800 dark:text-green-200 bg-green-100 dark:bg-green-900/40 border-green-300 dark:border-green-600`; + case 'Delete': + return `${baseClass} text-red-800 dark:text-red-200 bg-red-100 dark:bg-red-900/40 border-red-300 dark:border-red-600`; + case 'Equal': + default: + return `${baseClass} text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700`; + } + }; + + const getChunkPrefix = (chunkType: DiffChunkType) => { + switch (chunkType) { + case 'Insert': + return '+'; + case 'Delete': + return '-'; + case 'Equal': + default: + return ' '; + } + }; + + const processFileChunks = (chunks: DiffChunk[], fileIndex: number) => { + const CONTEXT_LINES = compact ? 2 : 3; + const lines: ProcessedLine[] = []; + let oldLineNumber = 1; + let newLineNumber = 1; + + // Convert chunks to lines with line numbers + chunks.forEach((chunk) => { + const chunkLines = chunk.content.split('\n'); + chunkLines.forEach((line, index) => { + if (index < chunkLines.length - 1 || line !== '') { + const processedLine: ProcessedLine = { + content: line, + chunkType: chunk.chunk_type, + }; + + switch (chunk.chunk_type) { + case 'Equal': + processedLine.oldLineNumber = oldLineNumber++; + processedLine.newLineNumber = newLineNumber++; + break; + case 'Delete': + processedLine.oldLineNumber = oldLineNumber++; + break; + case 'Insert': + processedLine.newLineNumber = newLineNumber++; + break; + } + + lines.push(processedLine); + } + }); + }); + + const sections: ProcessedSection[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + if (line.chunkType === 'Equal') { + let nextChangeIndex = i + 1; + while ( + nextChangeIndex < lines.length && + lines[nextChangeIndex].chunkType === 'Equal' + ) { + nextChangeIndex++; + } + + const contextLength = nextChangeIndex - i; + const hasNextChange = nextChangeIndex < lines.length; + const hasPrevChange = + sections.length > 0 && + sections[sections.length - 1].type === 'change'; + + if ( + contextLength <= CONTEXT_LINES * 2 || + (!hasPrevChange && !hasNextChange) + ) { + sections.push({ + type: 'context', + lines: lines.slice(i, nextChangeIndex), + }); + } else { + if (hasPrevChange) { + sections.push({ + type: 'context', + lines: lines.slice(i, i + CONTEXT_LINES), + }); + i += CONTEXT_LINES; + } + + if (hasNextChange) { + const expandStart = hasPrevChange ? i : i + CONTEXT_LINES; + const expandEnd = nextChangeIndex - CONTEXT_LINES; + + if (expandEnd > expandStart) { + const expandKey = `${fileIndex}-${expandStart}-${expandEnd}`; + const isExpanded = expandedSections.has(expandKey); + + if (isExpanded) { + sections.push({ + type: 'expanded', + lines: lines.slice(expandStart, expandEnd), + expandKey, + }); + } else { + sections.push({ + type: 'context', + lines: [], + expandKey, + }); + } + } + + sections.push({ + type: 'context', + lines: lines.slice( + nextChangeIndex - CONTEXT_LINES, + nextChangeIndex + ), + }); + } else if (!hasPrevChange) { + sections.push({ + type: 'context', + lines: lines.slice(i, i + CONTEXT_LINES), + }); + } + } + + i = nextChangeIndex; + } else { + const changeStart = i; + while (i < lines.length && lines[i].chunkType !== 'Equal') { + i++; + } + + sections.push({ + type: 'change', + lines: lines.slice(changeStart, i), + }); + } + } + + return sections; + }; + + const toggleExpandSection = (expandKey: string) => { + setExpandedSections((prev) => { + const newSet = new Set(prev); + if (newSet.has(expandKey)) { + newSet.delete(expandKey); + } else { + newSet.add(expandKey); + } + return newSet; + }); + }; + + const toggleFileCollapse = (filePath: string) => { + setCollapsedFiles((prev) => { + const newSet = new Set(prev); + if (newSet.has(filePath)) { + newSet.delete(filePath); + } else { + newSet.add(filePath); + } + return newSet; + }); + }; + + const collapseAllFiles = () => { + if (diff) { + setCollapsedFiles(new Set(diff.files.map((file) => file.path))); + } + }; + + const expandAllFiles = () => { + setCollapsedFiles(new Set()); + }; + + if (!diff || diff.files.length === 0) { + return ( +
+
+ +

No changes detected

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+ {diff.files.length} file{diff.files.length !== 1 ? 's' : ''} changed +
+ {isBackgroundRefreshing && ( +
+
+ + Updating... + +
+ )} +
+ {!compact && diff.files.length > 1 && ( +
+ + +
+ )} +
+ + {/* Files */} +
+
+ {diff.files.map((file, fileIndex) => ( +
+
+
+ +

+ {file.path} +

+ {collapsedFiles.has(file.path) && ( +
+ + + + {file.chunks + .filter((c) => c.chunk_type === 'Insert') + .reduce( + (acc, c) => acc + c.content.split('\n').length - 1, + 0 + )} + + + - + {file.chunks + .filter((c) => c.chunk_type === 'Delete') + .reduce( + (acc, c) => acc + c.content.split('\n').length - 1, + 0 + )} + +
+ )} +
+ {onDeleteFile && ( + + )} +
+ {!collapsedFiles.has(file.path) && ( +
+
+ {processFileChunks(file.chunks, fileIndex).map( + (section, sectionIndex) => { + if ( + section.type === 'context' && + section.lines.length === 0 && + section.expandKey + ) { + const lineCount = + parseInt(section.expandKey.split('-')[2]) - + parseInt(section.expandKey.split('-')[1]); + return ( +
+ +
+ ); + } + + return ( +
+ {section.type === 'expanded' && + section.expandKey && ( +
+ +
+ )} + {section.lines.map((line, lineIndex) => ( +
+
+ + {line.oldLineNumber || ''} + + + {line.newLineNumber || ''} + +
+
+ + {getChunkPrefix(line.chunkType)} + + + {line.content} + +
+
+ ))} +
+ ); + } + )} +
+
+ )} +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/tasks/NormalizedConversationViewer.tsx b/frontend/src/components/tasks/NormalizedConversationViewer.tsx index 84e8bd89..f20acd3b 100644 --- a/frontend/src/components/tasks/NormalizedConversationViewer.tsx +++ b/frontend/src/components/tasks/NormalizedConversationViewer.tsx @@ -20,18 +20,24 @@ import { } from 'lucide-react'; import { makeRequest } from '@/lib/api'; import { MarkdownRenderer } from '@/components/ui/markdown-renderer'; +import { DiffCard } from './DiffCard'; import type { NormalizedConversation, NormalizedEntry, NormalizedEntryType, ExecutionProcess, ApiResponse, + WorktreeDiff, } from 'shared/types'; interface NormalizedConversationViewerProps { executionProcess: ExecutionProcess; projectId: string; onConversationUpdate?: () => void; + diff?: WorktreeDiff | null; + isBackgroundRefreshing?: boolean; + onDeleteFile?: (filePath: string) => void; + deletingFiles?: Set; } const getEntryIcon = (entryType: NormalizedEntryType) => { @@ -207,6 +213,116 @@ const clusterGeminiMessages = ( return clustered; }; +// Helper function to determine if a tool call modifies files +const isFileModificationToolCall = ( + entryType: NormalizedEntryType +): boolean => { + if (entryType.type !== 'tool_use') { + return false; + } + + // Check for direct file write action + if (entryType.action_type.action === 'file_write') { + return true; + } + + // Check for "other" actions that are file modification tools + if (entryType.action_type.action === 'other') { + const fileModificationTools = [ + 'edit', + 'write', + 'create_file', + 'multiedit', + 'edit_file', + ]; + return fileModificationTools.includes( + entryType.tool_name?.toLowerCase() || '' + ); + } + + return false; +}; + +// Extract file path from tool call +const extractFilePathFromToolCall = (entry: NormalizedEntry): string | null => { + if (entry.entry_type.type !== 'tool_use') { + return null; + } + + const { action_type, tool_name } = entry.entry_type; + + // Direct path extraction from action_type + if (action_type.action === 'file_write') { + return action_type.path || null; + } + + // For "other" actions, check if it's a known file modification tool + if (action_type.action === 'other') { + const fileModificationTools = [ + 'edit', + 'write', + 'create_file', + 'multiedit', + 'edit_file', + ]; + + if (fileModificationTools.includes(tool_name.toLowerCase())) { + // Parse file path from content field + return parseFilePathFromContent(entry.content); + } + } + + return null; +}; + +// Parse file path from content (handles various formats) +const parseFilePathFromContent = (content: string): string | null => { + // Try to extract path from backticks: `path/to/file.ext` + const backtickMatch = content.match(/`([^`]+)`/); + if (backtickMatch) { + return backtickMatch[1]; + } + + // Try to extract from common patterns like "Edit file: path" or "Write file: path" + const actionMatch = content.match( + /(?:Edit|Write|Create)\s+file:\s*([^\s\n]+)/i + ); + if (actionMatch) { + return actionMatch[1]; + } + + return null; +}; + +// Create filtered diff showing only specific files +const createIncrementalDiff = ( + fullDiff: WorktreeDiff | null, + targetFilePaths: string[] +): WorktreeDiff | null => { + if (!fullDiff || targetFilePaths.length === 0) { + return null; + } + + // Filter files to only include the target file paths + const filteredFiles = fullDiff.files.filter((file) => + targetFilePaths.some( + (targetPath) => + file.path === targetPath || + file.path.endsWith('/' + targetPath) || + targetPath.endsWith('/' + file.path) + ) + ); + + if (filteredFiles.length === 0) { + return null; + } + + return { + ...fullDiff, + files: filteredFiles, + }; +}; + // Helper function to determine if content should be rendered as markdown const shouldRenderMarkdown = (entryType: NormalizedEntryType) => { // Render markdown for assistant messages and tool outputs that contain backticks @@ -242,6 +358,10 @@ export function NormalizedConversationViewer({ executionProcess, projectId, onConversationUpdate, + diff, + isBackgroundRefreshing = false, + onDeleteFile, + deletingFiles = new Set(), }: NormalizedConversationViewerProps) { const [conversation, setConversation] = useState(null); @@ -430,70 +550,105 @@ export function NormalizedConversationViewer({ const isExpanded = expandedErrors.has(index); const hasMultipleLines = isErrorMessage && entry.content.includes('\n'); + const isFileModification = isFileModificationToolCall( + entry.entry_type + ); + + // Extract file path from this specific tool call + const modifiedFilePath = isFileModification + ? extractFilePathFromToolCall(entry) + : null; + + // Create incremental diff showing only the files modified by this specific tool call + const incrementalDiff = + modifiedFilePath && diff + ? createIncrementalDiff(diff, [modifiedFilePath]) + : null; + + // Show incremental diff for this specific file modification + const shouldShowDiff = + isFileModification && + incrementalDiff && + incrementalDiff.files.length > 0; return ( -
-
- {isErrorMessage && hasMultipleLines ? ( - - ) : ( - getEntryIcon(entry.entry_type) - )} -
-
- {isErrorMessage && hasMultipleLines ? ( -
-
- {isExpanded ? ( - shouldRenderMarkdown(entry.entry_type) ? ( - +
+
+
+ {isErrorMessage && hasMultipleLines ? ( + + ) : ( + getEntryIcon(entry.entry_type) + )} +
+
+ {isErrorMessage && hasMultipleLines ? ( +
+
+ {isExpanded ? ( + shouldRenderMarkdown(entry.entry_type) ? ( + + ) : ( + entry.content + ) ) : ( - entry.content - ) - ) : ( - <> - {entry.content.split('\n')[0]} - - + <> + {entry.content.split('\n')[0]} + + + )} +
+ {isExpanded && ( + )}
- {isExpanded && ( - - )} -
- ) : ( -
- {shouldRenderMarkdown(entry.entry_type) ? ( - - ) : ( - entry.content - )} -
- )} + ) : ( +
+ {shouldRenderMarkdown(entry.entry_type) ? ( + + ) : ( + entry.content + )} +
+ )} +
+ + {/* Render incremental diff card inline after file modification entries */} + {shouldShowDiff && incrementalDiff && ( +
+ +
+ )}
); })} diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 8c2d9295..6b9dfa1e 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -20,20 +20,17 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { - FileText, ChevronDown, ChevronUp, - Trash2, - Eye, - EyeOff, + MessageSquare, + GitCompare, } from 'lucide-react'; +import { DiffCard } from './DiffCard'; import type { TaskWithAttemptStatus, EditorType, Project, WorktreeDiff, - DiffChunkType, - DiffChunk, } from 'shared/types'; interface TaskDetailsPanelProps { @@ -53,21 +50,6 @@ interface ApiResponse { message: string | null; } -interface ProcessedLine { - content: string; - chunkType: DiffChunkType; - oldLineNumber?: number; - newLineNumber?: number; -} - -interface ProcessedSection { - type: 'context' | 'change' | 'expanded'; - lines: ProcessedLine[]; - expandKey?: string; - expandedAbove?: boolean; - expandedBelow?: boolean; -} - export function TaskDetailsPanel({ task, project, @@ -84,18 +66,18 @@ export function TaskDetailsPanel({ 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 [expandedSections, setExpandedSections] = useState>( - new Set() - ); - const [collapsedFiles, setCollapsedFiles] = useState>(new Set()); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [fileToDelete, setFileToDelete] = useState(null); - const [showDiffs, setShowDiffs] = useState(true); // Use the custom hook for all task details logic const { @@ -132,6 +114,14 @@ export function TaskDetailsPanel({ // Use ref to track loading state to prevent dependency cycles const diffLoadingRef = useRef(false); + // Reset to logs tab when task changes + useEffect(() => { + if (task) { + 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) => { @@ -227,6 +217,10 @@ export function TaskDetailsPanel({ 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, @@ -234,6 +228,8 @@ export function TaskDetailsPanel({ isOpen, selectedAttempt, fetchDiff, + activeTab, + userSelectedTab, ]); // Handle ESC key locally to prevent global navigation @@ -259,7 +255,11 @@ export function TaskDetailsPanel({ // Auto-scroll to bottom when activities, execution processes, or conversation changes (for logs section) useEffect(() => { - if (shouldAutoScrollLogs && scrollContainerRef.current) { + if ( + shouldAutoScrollLogs && + scrollContainerRef.current && + activeTab === 'logs' + ) { scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; } @@ -268,6 +268,7 @@ export function TaskDetailsPanel({ attemptData.processes, conversationUpdateTrigger, shouldAutoScrollLogs, + activeTab, ]); // Auto-scroll setup script logs to bottom @@ -302,216 +303,6 @@ export function TaskDetailsPanel({ } }; - // Diff processing functions - const getChunkClassName = (chunkType: DiffChunkType) => { - const baseClass = 'font-mono text-sm whitespace-pre flex w-full'; - - switch (chunkType) { - case 'Insert': - return `${baseClass} bg-green-50 dark:bg-green-900/20 text-green-900 dark:text-green-100`; - case 'Delete': - return `${baseClass} bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-100`; - case 'Equal': - default: - return `${baseClass} text-muted-foreground`; - } - }; - - const getLineNumberClassName = (chunkType: DiffChunkType) => { - const baseClass = - 'flex-shrink-0 w-12 px-1.5 text-xs border-r select-none min-h-[1.25rem] flex items-center'; - - switch (chunkType) { - case 'Insert': - return `${baseClass} text-green-800 dark:text-green-200 bg-green-100 dark:bg-green-900/40 border-green-300 dark:border-green-600`; - case 'Delete': - return `${baseClass} text-red-800 dark:text-red-200 bg-red-100 dark:bg-red-900/40 border-red-300 dark:border-red-600`; - case 'Equal': - default: - return `${baseClass} text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700`; - } - }; - - const getChunkPrefix = (chunkType: DiffChunkType) => { - switch (chunkType) { - case 'Insert': - return '+'; - case 'Delete': - return '-'; - case 'Equal': - default: - return ' '; - } - }; - - const processFileChunks = (chunks: DiffChunk[], fileIndex: number) => { - const CONTEXT_LINES = 3; - const lines: ProcessedLine[] = []; - let oldLineNumber = 1; - let newLineNumber = 1; - - // Convert chunks to lines with line numbers - chunks.forEach((chunk) => { - const chunkLines = chunk.content.split('\n'); - chunkLines.forEach((line, index) => { - if (index < chunkLines.length - 1 || line !== '') { - const processedLine: ProcessedLine = { - content: line, - chunkType: chunk.chunk_type, - }; - - switch (chunk.chunk_type) { - case 'Equal': - processedLine.oldLineNumber = oldLineNumber++; - processedLine.newLineNumber = newLineNumber++; - break; - case 'Delete': - processedLine.oldLineNumber = oldLineNumber++; - break; - case 'Insert': - processedLine.newLineNumber = newLineNumber++; - break; - } - - lines.push(processedLine); - } - }); - }); - - const sections: ProcessedSection[] = []; - let i = 0; - - while (i < lines.length) { - const line = lines[i]; - - if (line.chunkType === 'Equal') { - let nextChangeIndex = i + 1; - while ( - nextChangeIndex < lines.length && - lines[nextChangeIndex].chunkType === 'Equal' - ) { - nextChangeIndex++; - } - - const contextLength = nextChangeIndex - i; - const hasNextChange = nextChangeIndex < lines.length; - const hasPrevChange = - sections.length > 0 && - sections[sections.length - 1].type === 'change'; - - if ( - contextLength <= CONTEXT_LINES * 2 || - (!hasPrevChange && !hasNextChange) - ) { - sections.push({ - type: 'context', - lines: lines.slice(i, nextChangeIndex), - }); - } else { - if (hasPrevChange) { - sections.push({ - type: 'context', - lines: lines.slice(i, i + CONTEXT_LINES), - }); - i += CONTEXT_LINES; - } - - if (hasNextChange) { - const expandStart = hasPrevChange ? i : i + CONTEXT_LINES; - const expandEnd = nextChangeIndex - CONTEXT_LINES; - - if (expandEnd > expandStart) { - const expandKey = `${fileIndex}-${expandStart}-${expandEnd}`; - const isExpanded = expandedSections.has(expandKey); - - if (isExpanded) { - sections.push({ - type: 'expanded', - lines: lines.slice(expandStart, expandEnd), - expandKey, - }); - } else { - sections.push({ - type: 'context', - lines: [], - expandKey, - }); - } - } - - sections.push({ - type: 'context', - lines: lines.slice( - nextChangeIndex - CONTEXT_LINES, - nextChangeIndex - ), - }); - } else if (!hasPrevChange) { - sections.push({ - type: 'context', - lines: lines.slice(i, i + CONTEXT_LINES), - }); - } - } - - i = nextChangeIndex; - } else { - const changeStart = i; - while (i < lines.length && lines[i].chunkType !== 'Equal') { - i++; - } - - sections.push({ - type: 'change', - lines: lines.slice(changeStart, i), - }); - } - } - - return sections; - }; - - const toggleExpandSection = (expandKey: string) => { - setExpandedSections((prev) => { - const newSet = new Set(prev); - if (newSet.has(expandKey)) { - newSet.delete(expandKey); - } else { - newSet.add(expandKey); - } - return newSet; - }); - }; - - const toggleFileCollapse = (filePath: string) => { - setCollapsedFiles((prev) => { - const newSet = new Set(prev); - if (newSet.has(filePath)) { - newSet.delete(filePath); - } else { - newSet.add(filePath); - } - return newSet; - }); - }; - - const collapseAllFiles = () => { - if (diff) { - setCollapsedFiles(new Set(diff.files.map((file) => file.path))); - } - }; - - const expandAllFiles = () => { - setCollapsedFiles(new Set()); - }; - - // Helper to check if all files with content are collapsed - const areAllFilesCollapsed = () => { - return ( - diff && diff.files.length > 0 && collapsedFiles.size === diff.files.length - ); - }; - const handleDeleteFileClick = (filePath: string) => { setFileToDelete(filePath); }; @@ -557,8 +348,58 @@ export function TaskDetailsPanel({ setFileToDelete(null); }; - // Determine what content to show based on execution state - const renderMainContent = (): JSX.Element => { + // 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 ( @@ -572,10 +413,10 @@ export function TaskDetailsPanel({ // If no attempt is selected, show message if (!selectedAttempt) { return ( -
-
-

No attempt selected

-
+
+ +

No attempt selected

+

Select an attempt to view its logs

); } @@ -583,10 +424,14 @@ export function TaskDetailsPanel({ // If no execution state, execution hasn't started yet if (!executionState) { return ( -
-
-

Task execution not started yet

-
+
+ +

+ Task execution not started yet +

+

+ Logs will appear here once the task execution begins +

); } @@ -613,10 +458,7 @@ export function TaskDetailsPanel({ ); return ( -
+

Setup Script Running

@@ -647,7 +489,7 @@ export function TaskDetailsPanel({ ); return ( -

+

Setup Script Failed @@ -679,7 +521,7 @@ export function TaskDetailsPanel({ ); return ( -

+

Coding Agent Failed @@ -709,11 +551,10 @@ export function TaskDetailsPanel({ !hasChanges ) { return ( -

-
-

Setup Complete

-

Waiting for coding agent to start...

-
+
+ +

Setup Complete

+

Waiting for coding agent to start...

); } @@ -721,511 +562,137 @@ export function TaskDetailsPanel({ // When task is complete, show completion message if (isComplete) { return ( -
-
-

Task Complete

-

- The task has been completed successfully. -

-
+
+ +

Task Complete

+

+ The task has been completed successfully. +

); } - // When coding agent is running or complete but no changes yet, show just agent conversation (full height) - if ((isCodingAgentRunning || isCodingAgentComplete) && !hasChanges) { + // 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( +
+ {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 (!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...

-
- ); - })() - )} -
-
- ); - } - - // When changes appear, show them (2/3) and conversation (1/3) in split view - if (hasChanges) { - return ( - <> - {/* Top area - Code Changes (responsive height) */} - {showDiffs && ( -
- {diffLoading ? ( -
-
-

- Loading changes... -

-
- ) : diffError ? ( -
-

{diffError}

-
- ) : !diff || diff.files.length === 0 ? ( -
- -

No changes detected

-

- The worktree is identical to the base commit -

-
- ) : ( -
-
-
-
- {diff.files.length} file - {diff.files.length !== 1 ? 's' : ''} changed -
- {isBackgroundRefreshing && ( -
-
- - Updating... - -
- )} -
-
- - {diff.files.length > 1 && ( - <> - - - - )} -
-
- {diff.files.map((file, fileIndex) => ( -
-
-
- -

- {file.path} -

- {collapsedFiles.has(file.path) && ( -
- - + - {file.chunks - .filter((c) => c.chunk_type === 'Insert') - .reduce( - (acc, c) => - acc + c.content.split('\n').length - 1, - 0 - )} - - - - - {file.chunks - .filter((c) => c.chunk_type === 'Delete') - .reduce( - (acc, c) => - acc + c.content.split('\n').length - 1, - 0 - )} - -
- )} -
- -
- {!collapsedFiles.has(file.path) && ( -
-
- {processFileChunks(file.chunks, fileIndex).map( - (section, sectionIndex) => { - if ( - section.type === 'context' && - section.lines.length === 0 && - section.expandKey - ) { - const lineCount = - parseInt(section.expandKey.split('-')[2]) - - parseInt(section.expandKey.split('-')[1]); - return ( -
- -
- ); - } - - return ( -
- {section.type === 'expanded' && - section.expandKey && ( -
- -
- )} - {section.lines.map((line, lineIndex) => ( -
-
- - {line.oldLineNumber || ''} - - - {line.newLineNumber || ''} - -
-
- - {getChunkPrefix(line.chunkType)} - - {line.content} -
-
- ))} -
- ); - } - )} -
-
- )} -
- ))} -
- )} -
- )} - - {/* Show Diffs button when diffs are hidden */} - {!showDiffs && hasChanges && ( -
-
- -
-
- )} - - {/* Bottom area - Agent Logs (responsive height) */} -
-
- {loading ? ( -
-
-

Loading...

-
- ) : ( - (() => { - // Find main coding agent process (command: "executor") - let mainCodingAgentProcess = Object.values( + if (mainCodingAgentSummary) { + mainCodingAgentProcess = Object.values( attemptData.runningProcessDetails - ).find( - (process) => - process.process_type === 'codingagent' && - process.command === 'executor' - ); + ).find((process) => process.id === mainCodingAgentSummary.id); 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) => ( -
-
- -
- ))} -
- ); + 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 ( -
-

No coding agent conversation to display

-
+ 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

-
+
+ +

Unknown execution state

); }; @@ -1250,34 +717,102 @@ export function TaskDetailsPanel({ onDeleteTask={onDeleteTask} /> - {/* Toolbar */} - + {/* Collapsible Toolbar */} +
+
+

+ Task Details +

+ +
+ {!isHeaderCollapsed && ( + + )} +
- {/* Main Content - Dynamic based on execution state */} -
- {renderMainContent()} + {/* Tab Navigation */} +
+
+ + +
+
+ + {/* Tab Content */} +
+ {renderTabContent()}
{/* Footer - Follow-up section */}