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:
committed by
GitHub
parent
47338fd6b1
commit
40df3d17fe
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user