Migrate the ProcessesTab (vibe-kanban) (#742)

* Swapped the tab to the streaming process hook so the list reflects live updates while keeping the on-demand detail fetch for logs.

- `frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx:24` now consumes `useExecutionProcesses`, clears cached detail state when the attempt changes, and falls back to streamed data for the selected process.
- `frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx:77` memoizes the detail fetch helper and prevents duplicate loads while a selection fetch is in-flight.
- `frontend/src/components/tasks/TaskDetails/ProcessesTab.tsx:142` refreshes the list rendering to cover loading/error/empty cases from the stream and keeps the detail pane behavior unchanged for logs.

Tests: `pnpm run frontend:check`

Next step: 1) open the task details view and confirm processes appear and update as new executions start/end.

* Cleanup script changes for task attempt 280ab641-e8e8-4a78-9aab-4ec7c78bcd55
This commit is contained in:
Louis Knight-Webb
2025-09-16 12:54:25 +01:00
committed by GitHub
parent 47338fd6b1
commit 40df3d17fe

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { import {
Play, Play,
Square, Square,
@@ -10,7 +10,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { executionProcessesApi } from '@/lib/api.ts'; import { executionProcessesApi } from '@/lib/api.ts';
import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx'; import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx';
import { useAttemptExecution } from '@/hooks'; import { useExecutionProcesses } from '@/hooks/useExecutionProcesses';
import ProcessLogsViewer from './ProcessLogsViewer'; import ProcessLogsViewer from './ProcessLogsViewer';
import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types'; import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types';
@@ -21,13 +21,24 @@ interface ProcessesTabProps {
} }
function ProcessesTab({ attemptId }: ProcessesTabProps) { function ProcessesTab({ attemptId }: ProcessesTabProps) {
const { attemptData } = useAttemptExecution(attemptId); const {
executionProcesses,
executionProcessesById,
isLoading: processesLoading,
isConnected,
error: processesError,
} = useExecutionProcesses(attemptId ?? '');
const { selectedProcessId, setSelectedProcessId } = useProcessSelection(); const { selectedProcessId, setSelectedProcessId } = useProcessSelection();
const [loadingProcessId, setLoadingProcessId] = useState<string | null>(null); const [loadingProcessId, setLoadingProcessId] = useState<string | null>(null);
const [localProcessDetails, setLocalProcessDetails] = useState< const [localProcessDetails, setLocalProcessDetails] = useState<
Record<string, ExecutionProcess> Record<string, ExecutionProcess>
>({}); >({});
useEffect(() => {
setLocalProcessDetails({});
setLoadingProcessId(null);
}, [attemptId]);
const getStatusIcon = (status: ExecutionProcessStatus) => { const getStatusIcon = (status: ExecutionProcessStatus) => {
switch (status) { switch (status) {
case 'running': case 'running':
@@ -63,7 +74,7 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
return date.toLocaleString(); return date.toLocaleString();
}; };
const fetchProcessDetails = async (processId: string) => { const fetchProcessDetails = useCallback(async (processId: string) => {
try { try {
setLoadingProcessId(processId); setLoadingProcessId(processId);
const result = await executionProcessesApi.getDetails(processId); const result = await executionProcessesApi.getDetails(processId);
@@ -77,48 +88,52 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
} catch (err) { } catch (err) {
console.error('Failed to fetch process details:', err); console.error('Failed to fetch process details:', err);
} finally { } finally {
setLoadingProcessId(null); setLoadingProcessId((current) =>
current === processId ? null : current
);
} }
}; }, []);
// Automatically fetch process details when selectedProcessId changes // Automatically fetch process details when selectedProcessId changes
useEffect(() => { useEffect(() => {
if (!attemptId || !selectedProcessId) {
return;
}
if ( if (
selectedProcessId && !localProcessDetails[selectedProcessId] &&
!attemptData.runningProcessDetails[selectedProcessId] && loadingProcessId !== selectedProcessId
!localProcessDetails[selectedProcessId]
) { ) {
fetchProcessDetails(selectedProcessId); fetchProcessDetails(selectedProcessId);
} }
}, [ }, [
attemptId,
selectedProcessId, selectedProcessId,
attemptData.runningProcessDetails,
localProcessDetails, localProcessDetails,
loadingProcessId,
fetchProcessDetails,
]); ]);
const handleProcessClick = async (process: ExecutionProcess) => { const handleProcessClick = async (process: ExecutionProcess) => {
setSelectedProcessId(process.id); setSelectedProcessId(process.id);
// If we don't have details for this process, fetch them // If we don't have details for this process, fetch them
if ( if (!localProcessDetails[process.id]) {
!attemptData.runningProcessDetails[process.id] &&
!localProcessDetails[process.id]
) {
await fetchProcessDetails(process.id); await fetchProcessDetails(process.id);
} }
}; };
const selectedProcess = selectedProcessId const selectedProcess = selectedProcessId
? attemptData.runningProcessDetails[selectedProcessId] || ? localProcessDetails[selectedProcessId] ||
localProcessDetails[selectedProcessId] executionProcessesById[selectedProcessId]
: null; : null;
if (!attemptData.processes || attemptData.processes.length === 0) { if (!attemptId) {
return ( return (
<div className="flex-1 flex items-center justify-center text-muted-foreground"> <div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center"> <div className="text-center">
<Cog className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Cog className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No execution processes found for this attempt.</p> <p>Select an attempt to view execution processes.</p>
</div> </div>
</div> </div>
); );
@@ -128,79 +143,101 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
<div className="flex-1 flex flex-col min-h-0"> <div className="flex-1 flex flex-col min-h-0">
{!selectedProcessId ? ( {!selectedProcessId ? (
<div className="flex-1 overflow-auto px-4 pb-20 pt-4"> <div className="flex-1 overflow-auto px-4 pb-20 pt-4">
<div className="space-y-3"> {processesError && (
{attemptData.processes.map((process) => ( <div className="mb-3 text-sm text-destructive">
<div Failed to load live updates for processes.
key={process.id} {!isConnected && ' Reconnecting...'}
className={`border rounded-lg p-4 hover:bg-muted/30 cursor-pointer transition-colors ${ </div>
loadingProcessId === process.id )}
? 'opacity-50 cursor-wait' {processesLoading && executionProcesses.length === 0 ? (
: '' <div className="flex items-center justify-center text-muted-foreground py-10">
}`} <p>Loading execution processes...</p>
onClick={() => handleProcessClick(process)} </div>
> ) : executionProcesses.length === 0 ? (
<div className="flex items-start justify-between"> <div className="flex items-center justify-center text-muted-foreground py-10">
<div className="flex items-center space-x-3"> <div className="text-center">
{getStatusIcon(process.status)} <Cog className="h-12 w-12 mx-auto mb-4 opacity-50" />
<div> <p>No execution processes found for this attempt.</p>
<h3 className="font-medium text-sm"> </div>
{process.run_reason} </div>
</h3> ) : (
<p className="text-sm text-muted-foreground mt-1"> <div className="space-y-3">
Process ID: {process.id} {executionProcesses.map((process) => (
</p> <div
{process.dropped && ( key={process.id}
<span className={`border rounded-lg p-4 hover:bg-muted/30 cursor-pointer transition-colors ${
className="inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700 border border-amber-200" loadingProcessId === process.id
title="Deleted by restore: timeline was restored to a checkpoint and later executions were removed" ? 'opacity-50 cursor-wait'
> : ''
Deleted }`}
</span> onClick={() => handleProcessClick(process)}
)} >
{ <div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
{getStatusIcon(process.status)}
<div>
<h3 className="font-medium text-sm">
{process.run_reason}
</h3>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Agent:{' '} Process ID: {process.id}
{process.executor_action.typ.type ===
'CodingAgentInitialRequest' ||
process.executor_action.typ.type ===
'CodingAgentFollowUpRequest' ? (
<ProfileVariantBadge
profileVariant={
process.executor_action.typ.executor_profile_id
}
/>
) : null}
</p> </p>
} {process.dropped && (
<span
className="inline-block mt-1 text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700 border border-amber-200"
title="Deleted by restore: timeline was restored to a checkpoint and later executions were removed"
>
Deleted
</span>
)}
{
<p className="text-sm text-muted-foreground mt-1">
Agent:{' '}
{process.executor_action.typ.type ===
'CodingAgentInitialRequest' ||
process.executor_action.typ.type ===
'CodingAgentFollowUpRequest' ? (
<ProfileVariantBadge
profileVariant={
process.executor_action.typ
.executor_profile_id
}
/>
) : null}
</p>
}
</div>
</div>
<div className="text-right">
<span
className={`inline-block px-2 py-1 text-xs font-medium border rounded-full ${getStatusColor(
process.status
)}`}
>
{process.status}
</span>
{process.exit_code !== null && (
<p className="text-xs text-muted-foreground mt-1">
Exit: {process.exit_code.toString()}
</p>
)}
</div> </div>
</div> </div>
<div className="text-right"> <div className="mt-3 text-xs text-muted-foreground">
<span <div className="flex justify-between">
className={`inline-block px-2 py-1 text-xs font-medium border rounded-full ${getStatusColor( <span>Started: {formatDate(process.started_at)}</span>
process.status {process.completed_at && (
)}`} <span>
> Completed: {formatDate(process.completed_at)}
{process.status} </span>
</span> )}
{process.exit_code !== null && ( </div>
<p className="text-xs text-muted-foreground mt-1"> <div className="mt-1">Process ID: {process.id}</div>
Exit: {process.exit_code.toString()}
</p>
)}
</div> </div>
</div> </div>
<div className="mt-3 text-xs text-muted-foreground"> ))}
<div className="flex justify-between"> </div>
<span>Started: {formatDate(process.started_at)}</span> )}
{process.completed_at && (
<span>Completed: {formatDate(process.completed_at)}</span>
)}
</div>
<div className="mt-1">Process ID: {process.id}</div>
</div>
</div>
))}
</div>
</div> </div>
) : ( ) : (
<div className="flex-1 flex flex-col min-h-0"> <div className="flex-1 flex flex-col min-h-0">