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 {
|
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">
|
||||||
|
|||||||
Reference in New Issue
Block a user