diff --git a/frontend/src/components/logs/VirtualizedList.tsx b/frontend/src/components/logs/VirtualizedList.tsx index a9f2f1d0..47589ad9 100644 --- a/frontend/src/components/logs/VirtualizedList.tsx +++ b/frontend/src/components/logs/VirtualizedList.tsx @@ -8,6 +8,7 @@ import { } from '@virtuoso.dev/message-list'; import { useEffect, useRef, useState } from 'react'; import DisplayConversationEntry from '../NormalizedConversation/DisplayConversationEntry'; +import { useEntries } from '@/contexts/EntriesContext'; import { AddEntryType, PatchTypeWithKey, @@ -69,11 +70,13 @@ const ItemContent: VirtuosoMessageListProps< const VirtualizedList = ({ attempt }: VirtualizedListProps) => { const [channelData, setChannelData] = useState(null); const [loading, setLoading] = useState(true); + const { setEntries, reset } = useEntries(); - // When attempt changes, set loading + // When attempt changes, set loading and reset entries useEffect(() => { setLoading(true); - }, [attempt.id]); + reset(); + }, [attempt.id, reset]); const onEntriesUpdated = ( newEntries: PatchTypeWithKey[], @@ -88,6 +91,7 @@ const VirtualizedList = ({ attempt }: VirtualizedListProps) => { } setChannelData({ data: newEntries, scrollModifier }); + setEntries(newEntries); // Update shared context if (loading) { setLoading(newLoading); } diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index be04d7e7..1f657abe 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -19,6 +19,7 @@ import TodoPanel from '@/components/tasks/TodoPanel'; import { TabNavContext } from '@/contexts/TabNavigationContext'; import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext'; import { ReviewProvider } from '@/contexts/ReviewProvider'; +import { EntriesProvider } from '@/contexts/EntriesContext'; import { AttemptHeaderCard } from './AttemptHeaderCard'; import { inIframe } from '@/vscode/bridge'; import { TaskRelationshipViewer } from './TaskRelationshipViewer'; @@ -117,87 +118,136 @@ export function TaskDetailsPanel({ - {/* Backdrop - only on smaller screens (overlay mode) */} - {!hideBackdrop && ( + + {/* Backdrop - only on smaller screens (overlay mode) */} + {!hideBackdrop && ( +
+ )} + + {/* Panel */}
- )} + className={ + className || getTaskPanelClasses(isFullScreen || false) + } + > +
+ {!inIframe() && ( + + )} - {/* Panel */} -
-
- {!inIframe() && ( - - )} + {isFullScreen ? ( +
+ {/* Sidebar */} + + {/* Main content */} +
+ {selectedAttempt && ( + <> + - {/* Main content */} -
- {selectedAttempt && ( +
+ {activeTab === 'diffs' ? ( + + ) : activeTab === 'processes' ? ( + + ) : ( + + )} +
+ + + + )} +
+
+ ) : ( + <> + {attempts.length === 0 ? ( + + ) : ( <> - { + // // TODO: Implement create new attempt + // console.log('Create new attempt'); + // }} + onJumpToDiffFullScreen={jumpToDiffFullScreen} /> -
- {activeTab === 'diffs' ? ( - - ) : activeTab === 'processes' ? ( - - ) : ( - - )} -
+ {selectedAttempt && ( + + )} )} - -
- ) : ( - <> - {attempts.length === 0 ? ( - - ) : ( - <> - { - // // TODO: Implement create new attempt - // console.log('Create new attempt'); - // }} - onJumpToDiffFullScreen={jumpToDiffFullScreen} - /> - - {selectedAttempt && ( - - )} - - - - )} - - )} + + )} +
-
+ diff --git a/frontend/src/components/tasks/TodoPanel.tsx b/frontend/src/components/tasks/TodoPanel.tsx index cacb9bb0..b5474cf5 100644 --- a/frontend/src/components/tasks/TodoPanel.tsx +++ b/frontend/src/components/tasks/TodoPanel.tsx @@ -1,10 +1,6 @@ -import { useMemo } from 'react'; import { Circle, CircleCheckBig, CircleDotDashed } from 'lucide-react'; -import { useProcessesLogs } from '@/hooks/useProcessesLogs'; +import { useEntries } from '@/contexts/EntriesContext'; import { usePinnedTodos } from '@/hooks/usePinnedTodos'; -import { useAttemptExecution } from '@/hooks'; -import { shouldShowInLogs } from '@/constants/processes'; -import type { TaskAttempt } from 'shared/types'; import { Card } from '../ui/card'; function getStatusIcon(status?: string) { @@ -16,26 +12,8 @@ function getStatusIcon(status?: string) { return ; } -interface TodoPanelProps { - selectedAttempt: TaskAttempt | null; -} - -export function TodoPanel({ selectedAttempt }: TodoPanelProps) { - const { attemptData } = useAttemptExecution(selectedAttempt?.id); - - const filteredProcesses = useMemo( - () => - (attemptData.processes || []).filter( - (p) => shouldShowInLogs(p.run_reason) && !p.dropped - ), - [ - attemptData.processes - ?.map((p) => `${p.id}:${p.status}:${p.dropped}`) - .join(','), - ] - ); - - const { entries } = useProcessesLogs(filteredProcesses, true); +export function TodoPanel() { + const { entries } = useEntries(); const { todos } = usePinnedTodos(entries); // Only show once the agent has created subtasks diff --git a/frontend/src/contexts/EntriesContext.tsx b/frontend/src/contexts/EntriesContext.tsx new file mode 100644 index 00000000..161bc714 --- /dev/null +++ b/frontend/src/contexts/EntriesContext.tsx @@ -0,0 +1,54 @@ +import { + createContext, + useContext, + useState, + useMemo, + useCallback, + ReactNode, +} from 'react'; +import type { PatchTypeWithKey } from '@/hooks/useConversationHistory'; + +interface EntriesContextType { + entries: PatchTypeWithKey[]; + setEntries: (entries: PatchTypeWithKey[]) => void; + reset: () => void; +} + +const EntriesContext = createContext(null); + +interface EntriesProviderProps { + children: ReactNode; +} + +export const EntriesProvider = ({ children }: EntriesProviderProps) => { + const [entries, setEntriesState] = useState([]); + + const setEntries = useCallback((newEntries: PatchTypeWithKey[]) => { + setEntriesState(newEntries); + }, []); + + const reset = useCallback(() => { + setEntriesState([]); + }, []); + + const value = useMemo( + () => ({ + entries, + setEntries, + reset, + }), + [entries, setEntries, reset] + ); + + return ( + {children} + ); +}; + +export const useEntries = (): EntriesContextType => { + const context = useContext(EntriesContext); + if (!context) { + throw new Error('useEntries must be used within an EntriesProvider'); + } + return context; +}; diff --git a/frontend/src/hooks/usePinnedTodos.ts b/frontend/src/hooks/usePinnedTodos.ts index 518a1b8f..b0156e46 100644 --- a/frontend/src/hooks/usePinnedTodos.ts +++ b/frontend/src/hooks/usePinnedTodos.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import type { TodoItem } from 'shared/types'; +import type { PatchTypeWithKey } from '@/hooks/useConversationHistory'; interface UsePinnedTodosResult { todos: TodoItem[]; @@ -10,14 +11,16 @@ interface UsePinnedTodosResult { * Hook that extracts and maintains the latest TODO state from normalized conversation entries. * Filters for TodoManagement ActionType entries and returns the most recent todo list. */ -export const usePinnedTodos = (entries: any[]): UsePinnedTodosResult => { +export const usePinnedTodos = ( + entries: PatchTypeWithKey[] +): UsePinnedTodosResult => { return useMemo(() => { let latestTodos: TodoItem[] = []; let lastUpdatedTime: string | null = null; for (const entry of entries) { - if (entry.channel === 'normalized' && entry.payload) { - const normalizedEntry = entry.payload as any; + if (entry.type === 'NORMALIZED_ENTRY' && entry.content) { + const normalizedEntry = entry.content as any; if ( normalizedEntry.entry_type?.type === 'tool_use' && diff --git a/frontend/src/hooks/useProcessesLogs.ts b/frontend/src/hooks/useProcessesLogs.ts deleted file mode 100644 index 83588c6b..00000000 --- a/frontend/src/hooks/useProcessesLogs.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { useMemo, useCallback } from 'react'; -import type { - ExecutionProcess, - NormalizedEntry, - PatchType, -} from 'shared/types'; -import type { UnifiedLogEntry, ProcessStartPayload } from '@/types/logs'; -import { useEventSourceManager } from './useEventSourceManager'; - -interface UseProcessesLogsResult { - entries: UnifiedLogEntry[]; - isConnected: boolean; - error: string | null; -} - -const MAX_ENTRIES = 5000; - -export const useProcessesLogs = ( - processes: ExecutionProcess[], - enabled: boolean -): UseProcessesLogsResult => { - const getEndpoint = useCallback((process: ExecutionProcess) => { - // Coding agents use normalized logs endpoint, scripts use raw logs endpoint - // Both endpoints now return PatchType objects via JSON patches - const isCodingAgent = process.run_reason === 'codingagent'; - return isCodingAgent - ? `/api/execution-processes/${process.id}/normalized-logs` - : `/api/execution-processes/${process.id}/raw-logs`; - }, []); - - const initialData = useMemo(() => ({ entries: [] }), []); - - const { processData, isConnected, error } = useEventSourceManager({ - processes, - enabled, - getEndpoint, - initialData, - }); - - const entries = useMemo(() => { - const allEntries: UnifiedLogEntry[] = []; - let entryCounter = 0; - - // Iterate through processes in order, adding process marker followed by logs - processes.forEach((process) => { - const data = processData[process.id]; - if (!data?.entries) return; - - // Add process start marker first - const processStartPayload: ProcessStartPayload = { - processId: process.id, - runReason: process.run_reason, - startedAt: process.started_at, - status: process.status, - action: process.executor_action, - }; - - allEntries.push({ - id: `${process.id}-start`, - ts: entryCounter++, - processId: process.id, - processName: process.run_reason, - channel: 'process_start', - payload: processStartPayload, - }); - - // Then add all logs for this process (skip the injected PROCESS_START entry) - data.entries.forEach( - ( - patchEntry: - | PatchType - | { type: 'PROCESS_START'; content: ProcessStartPayload }, - index: number - ) => { - // Skip the injected PROCESS_START entry since we handle it above - if (patchEntry.type === 'PROCESS_START') return; - - let channel: UnifiedLogEntry['channel']; - let payload: string | NormalizedEntry; - - switch (patchEntry.type) { - case 'STDOUT': - channel = 'stdout'; - payload = patchEntry.content; - break; - case 'STDERR': - channel = 'stderr'; - payload = patchEntry.content; - break; - case 'NORMALIZED_ENTRY': - channel = 'normalized'; - payload = patchEntry.content; - break; - default: - // Skip unknown patch types - return; - } - - allEntries.push({ - id: `${process.id}-${index}`, - ts: entryCounter++, - processId: process.id, - processName: process.run_reason, - channel, - payload, - }); - } - ); - }); - - // Limit entries (no sorting needed since we build in order) - return allEntries.slice(-MAX_ENTRIES); - }, [processData, processes]); - - return { entries, isConnected, error }; -};