chore: setup CI scripts (#6)
* wip: workflows * wip: fix up issues in ci scripts and fix frontend lint errors * wip: fix backend lints * remove unused deps * wip: build frontend in test.yml * wip: attempt to improve Rust caching * wip: testing release * wip: linear release flow * wip: check against both package.json versions * wip: spurious attempt to get Rust caching * wip: more cache * merge release and publish jobs; add more caching to release flow * decouple github releases and npm publishing * update pack flow --------- Co-authored-by: couscous <couscous@runner.com>
This commit is contained in:
committed by
GitHub
parent
b25f81504a
commit
340b094c75
@@ -7,7 +7,7 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 100",
|
||||
"lint:fix": "eslint . --ext ts,tsx --fix",
|
||||
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\""
|
||||
@@ -50,4 +50,4 @@
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
const [showEditForm, setShowEditForm] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const fetchProject = async () => {
|
||||
const fetchProject = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -55,7 +55,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [projectId]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!project) return;
|
||||
@@ -86,7 +86,7 @@ export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {
|
||||
|
||||
useEffect(() => {
|
||||
fetchProject();
|
||||
}, [projectId]);
|
||||
}, [fetchProject]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,10 @@ export function ExecutionOutputViewer({
|
||||
|
||||
// Check if stdout looks like JSONL (for Amp, Claude, or Gemini executor)
|
||||
const { isValidJsonl, jsonlFormat } = useMemo(() => {
|
||||
if ((!isAmpExecutor && !isClaudeExecutor && !isGeminiExecutor) || !executionProcess.stdout) {
|
||||
if (
|
||||
(!isAmpExecutor && !isClaudeExecutor && !isGeminiExecutor) ||
|
||||
!executionProcess.stdout
|
||||
) {
|
||||
return { isValidJsonl: false, jsonlFormat: null };
|
||||
}
|
||||
|
||||
@@ -99,7 +102,12 @@ export function ExecutionOutputViewer({
|
||||
} catch {
|
||||
return { isValidJsonl: false, jsonlFormat: null };
|
||||
}
|
||||
}, [isAmpExecutor, isClaudeExecutor, isGeminiExecutor, executionProcess.stdout]);
|
||||
}, [
|
||||
isAmpExecutor,
|
||||
isClaudeExecutor,
|
||||
isGeminiExecutor,
|
||||
executionProcess.stdout,
|
||||
]);
|
||||
|
||||
// Set initial view mode based on JSONL detection
|
||||
useEffect(() => {
|
||||
|
||||
@@ -86,9 +86,7 @@ export function TaskActivityHistory({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-3 block">
|
||||
Activity History
|
||||
</Label>
|
||||
<Label className="text-sm font-medium mb-3 block">Activity History</Label>
|
||||
{activities.length === 0 ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
No activities found
|
||||
|
||||
@@ -112,7 +112,7 @@ export function TaskDetailsHeader({
|
||||
<TooltipContent>
|
||||
<p>Close panel</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
getTaskPanelClasses,
|
||||
getBackdropClasses,
|
||||
} from '@/lib/responsive-config';
|
||||
import type {
|
||||
TaskWithAttemptStatus,
|
||||
EditorType,
|
||||
Project,
|
||||
} from 'shared/types';
|
||||
import type { TaskWithAttemptStatus, EditorType, Project } from 'shared/types';
|
||||
|
||||
interface TaskDetailsPanelProps {
|
||||
task: TaskWithAttemptStatus | null;
|
||||
|
||||
@@ -105,9 +105,7 @@ export function TaskDetailsToolbar({
|
||||
<div className="h-4 w-px bg-border" />
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No attempts yet
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">No attempts yet</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -171,9 +169,7 @@ export function TaskDetailsToolbar({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isStopping
|
||||
? 'Stopping execution...'
|
||||
: 'Stop execution'}
|
||||
{isStopping ? 'Stopping execution...' : 'Stop execution'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -230,8 +226,7 @@ export function TaskDetailsToolbar({
|
||||
}
|
||||
>
|
||||
{executor.name}
|
||||
{config?.executor.type === executor.id &&
|
||||
' (Default)'}
|
||||
{config?.executor.type === executor.id && ' (Default)'}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
@@ -257,16 +252,14 @@ export function TaskDetailsToolbar({
|
||||
onMouseLeave={() => onSetIsHoveringDevServer(false)}
|
||||
>
|
||||
<Button
|
||||
variant={
|
||||
runningDevServer ? 'destructive' : 'outline'
|
||||
}
|
||||
variant={runningDevServer ? 'destructive' : 'outline'}
|
||||
size="sm"
|
||||
onClick={
|
||||
runningDevServer ? onStopDevServer : onStartDevServer
|
||||
}
|
||||
disabled={
|
||||
isStartingDevServer || !project?.dev_script
|
||||
runningDevServer
|
||||
? onStopDevServer
|
||||
: onStartDevServer
|
||||
}
|
||||
disabled={isStartingDevServer || !project?.dev_script}
|
||||
>
|
||||
{runningDevServer ? (
|
||||
<StopCircle className="h-4 w-4" />
|
||||
|
||||
@@ -66,9 +66,7 @@ export function TaskFollowUpSection({
|
||||
<Button
|
||||
onClick={onSendFollowUp}
|
||||
disabled={
|
||||
!canSendFollowUp ||
|
||||
!followUpMessage.trim() ||
|
||||
isSendingFollowUp
|
||||
!canSendFollowUp || !followUpMessage.trim() || isSendingFollowUp
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -103,7 +103,7 @@ export function TaskFormDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAndStart = async () => {
|
||||
const handleCreateAndStart = useCallback(async () => {
|
||||
if (!title.trim()) return;
|
||||
|
||||
setIsSubmittingAndStart(true);
|
||||
@@ -121,9 +121,16 @@ export function TaskFormDialog({
|
||||
} finally {
|
||||
setIsSubmittingAndStart(false);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
title,
|
||||
description,
|
||||
config?.executor,
|
||||
isEditMode,
|
||||
onCreateAndStartTask,
|
||||
onOpenChange,
|
||||
]);
|
||||
|
||||
const handleCancel = () => {
|
||||
const handleCancel = useCallback(() => {
|
||||
// Reset form state when canceling
|
||||
if (task) {
|
||||
setTitle(task.title);
|
||||
@@ -135,7 +142,7 @@ export function TaskFormDialog({
|
||||
setStatus('todo');
|
||||
}
|
||||
onOpenChange(false);
|
||||
};
|
||||
}, [task, onOpenChange]);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
|
||||
@@ -56,7 +56,10 @@ export function useTaskDetails(
|
||||
return false;
|
||||
}
|
||||
|
||||
const latestActivitiesByProcess = new Map<string, TaskAttemptActivityWithPrompt>();
|
||||
const latestActivitiesByProcess = new Map<
|
||||
string,
|
||||
TaskAttemptActivityWithPrompt
|
||||
>();
|
||||
|
||||
attemptData.activities.forEach((activity) => {
|
||||
const existing = latestActivitiesByProcess.get(
|
||||
@@ -110,70 +113,78 @@ export function useTaskDetails(
|
||||
const lines = allOutput.split('\n').filter((line) => line.trim());
|
||||
const lastLines = lines.slice(-10);
|
||||
return lastLines.length > 0 ? lastLines.join('\n') : 'No output yet...';
|
||||
}, [devServerDetails?.stdout, devServerDetails?.stderr]);
|
||||
}, [devServerDetails]);
|
||||
|
||||
// Set default executor from config
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setSelectedExecutor(config.executor.type);
|
||||
}
|
||||
}, [config]);
|
||||
// Define callbacks first
|
||||
const fetchAttemptData = useCallback(
|
||||
async (attemptId: string) => {
|
||||
if (!task) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (task && isOpen) {
|
||||
fetchTaskAttempts();
|
||||
}
|
||||
}, [task, isOpen]);
|
||||
try {
|
||||
const [activitiesResponse, processesResponse] = await Promise.all([
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`
|
||||
),
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/execution-processes`
|
||||
),
|
||||
]);
|
||||
|
||||
// Polling for updates when attempt is running
|
||||
useEffect(() => {
|
||||
if (!isAttemptRunning || !task) return;
|
||||
if (activitiesResponse.ok && processesResponse.ok) {
|
||||
const activitiesResult: ApiResponse<TaskAttemptActivityWithPrompt[]> =
|
||||
await activitiesResponse.json();
|
||||
const processesResult: ApiResponse<ExecutionProcessSummary[]> =
|
||||
await processesResponse.json();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchAttemptData(selectedAttempt.id, true);
|
||||
}
|
||||
}, 2000);
|
||||
if (
|
||||
activitiesResult.success &&
|
||||
processesResult.success &&
|
||||
activitiesResult.data &&
|
||||
processesResult.data
|
||||
) {
|
||||
const runningActivities = activitiesResult.data.filter(
|
||||
(activity) =>
|
||||
activity.status === 'setuprunning' ||
|
||||
activity.status === 'executorrunning'
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAttemptRunning, task?.id, selectedAttempt?.id]);
|
||||
const runningProcessDetails: Record<string, ExecutionProcess> = {};
|
||||
for (const activity of runningActivities) {
|
||||
try {
|
||||
const detailResponse = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${activity.execution_process_id}`
|
||||
);
|
||||
if (detailResponse.ok) {
|
||||
const detailResult: ApiResponse<ExecutionProcess> =
|
||||
await detailResponse.json();
|
||||
if (detailResult.success && detailResult.data) {
|
||||
runningProcessDetails[activity.execution_process_id] =
|
||||
detailResult.data;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to fetch execution process ${activity.execution_process_id}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch dev server details when hovering
|
||||
const fetchDevServerDetails = useCallback(async () => {
|
||||
if (!runningDevServer || !task || !selectedAttempt) return;
|
||||
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${runningDevServer.id}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<ExecutionProcess> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setDevServerDetails(result.data);
|
||||
setAttemptData({
|
||||
activities: activitiesResult.data,
|
||||
processes: processesResult.data,
|
||||
runningProcessDetails,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch attempt data:', err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch dev server details:', err);
|
||||
}
|
||||
}, [runningDevServer?.id, task?.id, selectedAttempt?.id, projectId]);
|
||||
},
|
||||
[task, projectId]
|
||||
);
|
||||
|
||||
// Poll dev server details while hovering
|
||||
useEffect(() => {
|
||||
if (!isHoveringDevServer || !runningDevServer) {
|
||||
setDevServerDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchDevServerDetails();
|
||||
const interval = setInterval(fetchDevServerDetails, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [
|
||||
isHoveringDevServer,
|
||||
runningDevServer?.id,
|
||||
fetchDevServerDetails,
|
||||
]);
|
||||
|
||||
const fetchTaskAttempts = async () => {
|
||||
const fetchTaskAttempts = useCallback(async () => {
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
@@ -210,75 +221,64 @@ export function useTaskDetails(
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [task, projectId, fetchAttemptData]);
|
||||
|
||||
const fetchAttemptData = async (
|
||||
attemptId: string,
|
||||
_isBackgroundUpdate = false
|
||||
) => {
|
||||
if (!task) return;
|
||||
// Fetch dev server details when hovering
|
||||
const fetchDevServerDetails = useCallback(async () => {
|
||||
if (!runningDevServer || !task || !selectedAttempt) return;
|
||||
|
||||
try {
|
||||
const [activitiesResponse, processesResponse] = await Promise.all([
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/activities`
|
||||
),
|
||||
makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/execution-processes`
|
||||
),
|
||||
]);
|
||||
|
||||
if (activitiesResponse.ok && processesResponse.ok) {
|
||||
const activitiesResult: ApiResponse<TaskAttemptActivityWithPrompt[]> =
|
||||
await activitiesResponse.json();
|
||||
const processesResult: ApiResponse<ExecutionProcessSummary[]> =
|
||||
await processesResponse.json();
|
||||
|
||||
if (
|
||||
activitiesResult.success &&
|
||||
processesResult.success &&
|
||||
activitiesResult.data &&
|
||||
processesResult.data
|
||||
) {
|
||||
const runningActivities = activitiesResult.data.filter(
|
||||
(activity) =>
|
||||
activity.status === 'setuprunning' ||
|
||||
activity.status === 'executorrunning'
|
||||
);
|
||||
|
||||
const runningProcessDetails: Record<string, ExecutionProcess> = {};
|
||||
for (const activity of runningActivities) {
|
||||
try {
|
||||
const detailResponse = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${activity.execution_process_id}`
|
||||
);
|
||||
if (detailResponse.ok) {
|
||||
const detailResult: ApiResponse<ExecutionProcess> =
|
||||
await detailResponse.json();
|
||||
if (detailResult.success && detailResult.data) {
|
||||
runningProcessDetails[activity.execution_process_id] =
|
||||
detailResult.data;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to fetch execution process ${activity.execution_process_id}:`,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setAttemptData({
|
||||
activities: activitiesResult.data,
|
||||
processes: processesResult.data,
|
||||
runningProcessDetails,
|
||||
});
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/execution-processes/${runningDevServer.id}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<ExecutionProcess> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
setDevServerDetails(result.data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch attempt data:', err);
|
||||
console.error('Failed to fetch dev server details:', err);
|
||||
}
|
||||
};
|
||||
}, [runningDevServer, task, selectedAttempt, projectId]);
|
||||
|
||||
// Set default executor from config
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
setSelectedExecutor(config.executor.type);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (task && isOpen) {
|
||||
fetchTaskAttempts();
|
||||
}
|
||||
}, [task, isOpen, fetchTaskAttempts]);
|
||||
|
||||
// Polling for updates when attempt is running
|
||||
useEffect(() => {
|
||||
if (!isAttemptRunning || !task) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (selectedAttempt) {
|
||||
fetchAttemptData(selectedAttempt.id);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isAttemptRunning, task, selectedAttempt, fetchAttemptData]);
|
||||
|
||||
// Poll dev server details while hovering
|
||||
useEffect(() => {
|
||||
if (!isHoveringDevServer || !runningDevServer) {
|
||||
setDevServerDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchDevServerDetails();
|
||||
const interval = setInterval(fetchDevServerDetails, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isHoveringDevServer, runningDevServer, fetchDevServerDetails]);
|
||||
|
||||
const handleAttemptChange = (attemptId: string) => {
|
||||
const attempt = taskAttempts.find((a) => a.id === attemptId);
|
||||
@@ -482,13 +482,13 @@ export function useTaskDetails(
|
||||
isStartingDevServer,
|
||||
devServerDetails,
|
||||
isHoveringDevServer,
|
||||
|
||||
|
||||
// Computed
|
||||
runningDevServer,
|
||||
isAttemptRunning,
|
||||
canSendFollowUp,
|
||||
processedDevServerLogs,
|
||||
|
||||
|
||||
// Actions
|
||||
setSelectedExecutor,
|
||||
setFollowUpMessage,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@@ -90,7 +90,7 @@ export function ProjectTasks() {
|
||||
}
|
||||
}, [taskId, tasks]);
|
||||
|
||||
const fetchProject = async () => {
|
||||
const fetchProject = useCallback(async () => {
|
||||
try {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/with-branch`
|
||||
@@ -108,53 +108,56 @@ export function ProjectTasks() {
|
||||
} catch (err) {
|
||||
setError('Failed to load project');
|
||||
}
|
||||
};
|
||||
}, [projectId, navigate]);
|
||||
|
||||
const fetchTasks = async (skipLoading = false) => {
|
||||
try {
|
||||
if (!skipLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks`);
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Task[]> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
// Only update if data has actually changed
|
||||
setTasks((prevTasks) => {
|
||||
const newTasks = result.data!;
|
||||
if (JSON.stringify(prevTasks) === JSON.stringify(newTasks)) {
|
||||
return prevTasks; // Return same reference to prevent re-render
|
||||
}
|
||||
|
||||
// Update selectedTask if it exists and has been modified
|
||||
if (selectedTask) {
|
||||
const updatedSelectedTask = newTasks.find(
|
||||
(task) => task.id === selectedTask.id
|
||||
);
|
||||
if (
|
||||
updatedSelectedTask &&
|
||||
JSON.stringify(selectedTask) !==
|
||||
JSON.stringify(updatedSelectedTask)
|
||||
) {
|
||||
setSelectedTask(updatedSelectedTask);
|
||||
}
|
||||
}
|
||||
|
||||
return newTasks;
|
||||
});
|
||||
const fetchTasks = useCallback(
|
||||
async (skipLoading = false) => {
|
||||
try {
|
||||
if (!skipLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
} else {
|
||||
const response = await makeRequest(`/api/projects/${projectId}/tasks`);
|
||||
|
||||
if (response.ok) {
|
||||
const result: ApiResponse<Task[]> = await response.json();
|
||||
if (result.success && result.data) {
|
||||
// Only update if data has actually changed
|
||||
setTasks((prevTasks) => {
|
||||
const newTasks = result.data!;
|
||||
if (JSON.stringify(prevTasks) === JSON.stringify(newTasks)) {
|
||||
return prevTasks; // Return same reference to prevent re-render
|
||||
}
|
||||
|
||||
// Update selectedTask if it exists and has been modified
|
||||
if (selectedTask) {
|
||||
const updatedSelectedTask = newTasks.find(
|
||||
(task) => task.id === selectedTask.id
|
||||
);
|
||||
if (
|
||||
updatedSelectedTask &&
|
||||
JSON.stringify(selectedTask) !==
|
||||
JSON.stringify(updatedSelectedTask)
|
||||
) {
|
||||
setSelectedTask(updatedSelectedTask);
|
||||
}
|
||||
}
|
||||
|
||||
return newTasks;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError('Failed to load tasks');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load tasks');
|
||||
} finally {
|
||||
if (!skipLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load tasks');
|
||||
} finally {
|
||||
if (!skipLoading) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[projectId, selectedTask]
|
||||
);
|
||||
|
||||
const handleCreateTask = async (title: string, description: string) => {
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -59,14 +59,8 @@ export function TaskAttemptComparePage() {
|
||||
const [fileToDelete, setFileToDelete] = useState<string | null>(null);
|
||||
const [showUncommittedWarning, setShowUncommittedWarning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && taskId && attemptId) {
|
||||
fetchDiff();
|
||||
fetchBranchStatus();
|
||||
}
|
||||
}, [projectId, taskId, attemptId]);
|
||||
|
||||
const fetchDiff = async () => {
|
||||
// Define callbacks first
|
||||
const fetchDiff = useCallback(async () => {
|
||||
if (!projectId || !taskId || !attemptId) return;
|
||||
|
||||
try {
|
||||
@@ -90,9 +84,9 @@ export function TaskAttemptComparePage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [projectId, taskId, attemptId]);
|
||||
|
||||
const fetchBranchStatus = async () => {
|
||||
const fetchBranchStatus = useCallback(async () => {
|
||||
if (!projectId || !taskId || !attemptId) return;
|
||||
|
||||
try {
|
||||
@@ -116,7 +110,14 @@ export function TaskAttemptComparePage() {
|
||||
} finally {
|
||||
setBranchStatusLoading(false);
|
||||
}
|
||||
};
|
||||
}, [projectId, taskId, attemptId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && taskId && attemptId) {
|
||||
fetchDiff();
|
||||
fetchBranchStatus();
|
||||
}
|
||||
}, [projectId, taskId, attemptId, fetchDiff, fetchBranchStatus]);
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate(`/projects/${projectId}/tasks/${taskId}`);
|
||||
|
||||
Reference in New Issue
Block a user