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