diff --git a/frontend/src/components/tasks/TaskDetails/ProcessLogsViewer.tsx b/frontend/src/components/tasks/TaskDetails/ProcessLogsViewer.tsx index 774a28f8..eda1e999 100644 --- a/frontend/src/components/tasks/TaskDetails/ProcessLogsViewer.tsx +++ b/frontend/src/components/tasks/TaskDetails/ProcessLogsViewer.tsx @@ -65,37 +65,31 @@ export default function ProcessLogsViewer({ }; return ( -
-
-

Process Logs

-
- -
- {logs.length === 0 && !error ? ( -
- No logs available -
- ) : error ? ( -
- - {error} -
- ) : ( - - ref={virtuosoRef} - className="flex-1 rounded-lg" - data={logs} - itemContent={(index, entry) => - formatLogLine(entry as LogEntry, index) - } - // Keep pinned while user is at bottom; release when they scroll up - atBottomStateChange={setAtBottom} - followOutput={atBottom ? 'smooth' : false} - // Optional: a bit more overscan helps during bursts - increaseViewportBy={{ top: 0, bottom: 600 }} - /> - )} -
+
+ {logs.length === 0 && !error ? ( +
+ No logs available +
+ ) : error ? ( +
+ + {error} +
+ ) : ( + + ref={virtuosoRef} + className="flex-1 rounded-lg" + data={logs} + itemContent={(index, entry) => + formatLogLine(entry as LogEntry, index) + } + // Keep pinned while user is at bottom; release when they scroll up + atBottomStateChange={setAtBottom} + followOutput={atBottom ? 'smooth' : false} + // Optional: a bit more overscan helps during bursts + increaseViewportBy={{ top: 0, bottom: 600 }} + /> + )}
); } diff --git a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx index a7efb881..d6189f5c 100644 --- a/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx +++ b/frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react'; +import { useContext, useState, useEffect } from 'react'; import { Play, Square, @@ -13,12 +13,11 @@ import { executionProcessesApi } from '@/lib/api.ts'; import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx'; import ProcessLogsViewer from './ProcessLogsViewer'; import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types'; +import { useProcessSelection } from '@/contexts/ProcessSelectionContext'; function ProcessesTab() { const { attemptData, setAttemptData } = useContext(TaskAttemptDataContext); - const [selectedProcessId, setSelectedProcessId] = useState( - null - ); + const { selectedProcessId, setSelectedProcessId } = useProcessSelection(); const [loadingProcessId, setLoadingProcessId] = useState(null); const getStatusIcon = (status: ExecutionProcessStatus) => { @@ -77,6 +76,16 @@ function ProcessesTab() { } }; + // Automatically fetch process details when selectedProcessId changes + useEffect(() => { + if ( + selectedProcessId && + !attemptData.runningProcessDetails[selectedProcessId] + ) { + fetchProcessDetails(selectedProcessId); + } + }, [selectedProcessId, attemptData.runningProcessDetails]); + const handleProcessClick = async (process: ExecutionProcess) => { setSelectedProcessId(process.id); @@ -174,7 +183,7 @@ function ProcessesTab() {
) : (
-
+

Process Details

-
+
{selectedProcess ? ( -
-
-
-

Process Info

-
-

- Type:{' '} - {selectedProcess.run_reason} -

-

- Status:{' '} - {selectedProcess.status} -

- {/* Executor type field not available in new type */} -

- Exit Code:{' '} - {selectedProcess.exit_code?.toString() ?? 'N/A'} -

- {selectedProcess.executor_action.typ.type === - 'CodingAgentInitialRequest' || - selectedProcess.executor_action.typ.type === - 'CodingAgentFollowUpRequest' ? ( -

- Profile:{' '} - -

- ) : null} -
-
-
-

Timing

-
-

- Started:{' '} - {formatDate(selectedProcess.started_at)} -

- {selectedProcess.completed_at && ( -

- Completed:{' '} - {formatDate(selectedProcess.completed_at)} -

- )} -
-
-
- - {/* Command, working directory, stdout, stderr fields not available in new ExecutionProcess type */} -
-

- Process Information -

-
-
Process ID: {selectedProcess.id}
-
- Task Attempt ID: {selectedProcess.task_attempt_id} -
-
Run Reason: {selectedProcess.run_reason}
-
Status: {selectedProcess.status}
- {selectedProcess.exit_code !== null && ( -
- Exit Code: {selectedProcess.exit_code.toString()} -
- )} -
-
- - -
+ ) : loadingProcessId === selectedProcessId ? (

Loading process details...

diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 0c9e94d3..55742ba1 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -15,6 +15,8 @@ import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmat import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx'; import TaskDetailsProvider from '../context/TaskDetailsContextProvider.tsx'; import TaskDetailsToolbar from './TaskDetailsToolbar.tsx'; +import { TabNavContext } from '@/contexts/TabNavigationContext'; +import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext'; interface TaskDetailsPanelProps { task: TaskWithAttemptStatus | null; @@ -79,51 +81,55 @@ export function TaskDetailsPanel({ setShowEditorDialog={setShowEditorDialog} projectHasDevScript={projectHasDevScript} > - {/* Backdrop - only on smaller screens (overlay mode) */} - {!hideBackdrop && ( -
- )} - - {/* Panel */} -
-
- {!hideHeader && ( - + + + {/* Backdrop - only on smaller screens (overlay mode) */} + {!hideBackdrop && ( +
)} - + {/* Panel */} +
+
+ {!hideHeader && ( + + )} - + - {/* Tab Content */} -
- {activeTab === 'diffs' ? ( - - ) : activeTab === 'processes' ? ( - - ) : ( - - )} + + + {/* Tab Content */} +
+ {activeTab === 'diffs' ? ( + + ) : activeTab === 'processes' ? ( + + ) : ( + + )} +
+ + +
- -
-
+ setShowEditorDialog(false)} + /> - setShowEditorDialog(false)} - /> - - + +
+
)} diff --git a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx index 2525b934..9366342b 100644 --- a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx +++ b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx @@ -9,6 +9,7 @@ import { RefreshCw, Settings, StopCircle, + ScrollText, } from 'lucide-react'; import { Tooltip, @@ -51,6 +52,7 @@ import { } from '@/components/context/taskDetailsContext.ts'; import { useConfig } from '@/components/config-provider.tsx'; import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts.ts'; +import { useProcessSelection } from '@/contexts/ProcessSelectionContext'; // Helper function to get the display name for different editor types function getEditorDisplayName(editorType: string): string { @@ -104,6 +106,7 @@ function CurrentAttempt({ const { attemptData, fetchAttemptData, isAttemptRunning } = useContext( TaskAttemptDataContext ); + const { jumpToProcess } = useProcessSelection(); const [isStartingDevServer, setIsStartingDevServer] = useState(false); const [merging, setMerging] = useState(false); @@ -135,6 +138,16 @@ function CurrentAttempt({ ); }, [attemptData.processes]); + // Find latest dev server process (for logs viewing) + const latestDevServerProcess = useMemo(() => { + return [...attemptData.processes] + .filter((process) => process.run_reason === 'devserver') + .sort( + (a, b) => + new Date(b.started_at).getTime() - new Date(a.started_at).getTime() + )[0]; + }, [attemptData.processes]); + const fetchDevServerDetails = useCallback(async () => { if (!runningDevServer || !task || !selectedAttempt) return; @@ -189,6 +202,12 @@ function CurrentAttempt({ } }; + const handleViewDevServerLogs = () => { + if (latestDevServerProcess) { + jumpToProcess(latestDevServerProcess.id); + } + }; + const stopAllExecutions = useCallback(async () => { if (!task || !selectedAttempt || !isAttemptRunning) return; @@ -556,6 +575,27 @@ function CurrentAttempt({ + + {/* View Dev Server Logs Button */} + {latestDevServerProcess && ( + + + + + + +

View dev server logs

+
+
+
+ )}
diff --git a/frontend/src/contexts/ProcessSelectionContext.tsx b/frontend/src/contexts/ProcessSelectionContext.tsx new file mode 100644 index 00000000..4ac7a49e --- /dev/null +++ b/frontend/src/contexts/ProcessSelectionContext.tsx @@ -0,0 +1,64 @@ +import { + createContext, + useContext, + useState, + useMemo, + useCallback, + ReactNode, +} from 'react'; +import { useTabNavigation } from './TabNavigationContext'; + +interface ProcessSelectionContextType { + selectedProcessId: string | null; + setSelectedProcessId: (id: string | null) => void; + jumpToProcess: (processId: string) => void; +} + +const ProcessSelectionContext = + createContext(null); + +interface ProcessSelectionProviderProps { + children: ReactNode; +} + +export function ProcessSelectionProvider({ + children, +}: ProcessSelectionProviderProps) { + const { setActiveTab } = useTabNavigation(); + const [selectedProcessId, setSelectedProcessId] = useState( + null + ); + + const jumpToProcess = useCallback( + (processId: string) => { + setSelectedProcessId(processId); + setActiveTab('processes'); + }, + [setActiveTab] + ); + + const value = useMemo( + () => ({ + selectedProcessId, + setSelectedProcessId, + jumpToProcess, + }), + [selectedProcessId, setSelectedProcessId, jumpToProcess] + ); + + return ( + + {children} + + ); +} + +export const useProcessSelection = () => { + const context = useContext(ProcessSelectionContext); + if (!context) { + throw new Error( + 'useProcessSelection must be used within ProcessSelectionProvider' + ); + } + return context; +}; diff --git a/frontend/src/contexts/TabNavigationContext.tsx b/frontend/src/contexts/TabNavigationContext.tsx new file mode 100644 index 00000000..4062f576 --- /dev/null +++ b/frontend/src/contexts/TabNavigationContext.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext } from 'react'; +import type { TabType } from '@/types/tabs'; + +interface TabNavContextType { + activeTab: TabType; + setActiveTab: (tab: TabType) => void; +} + +export const TabNavContext = createContext(null); + +export const useTabNavigation = () => { + const context = useContext(TabNavContext); + if (!context) { + throw new Error('useTabNavigation must be used within TabNavContext'); + } + return context; +};