diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3d642d18..d3f8a3c7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -161,6 +161,10 @@ function AppContent() {
path="/projects/:projectId/tasks"
element={}
/>
+ }
+ />
}
diff --git a/frontend/src/components/tasks/TaskDetailsToolbar.tsx b/frontend/src/components/tasks/TaskDetailsToolbar.tsx
index 22edc87c..e7e130dd 100644
--- a/frontend/src/components/tasks/TaskDetailsToolbar.tsx
+++ b/frontend/src/components/tasks/TaskDetailsToolbar.tsx
@@ -6,7 +6,7 @@ import {
useReducer,
useState,
} from 'react';
-import { useLocation } from 'react-router-dom';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Play } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { attemptsApi, projectsApi } from '@/lib/api';
@@ -92,6 +92,8 @@ function TaskDetailsToolbar() {
const [selectedProfile, setSelectedProfile] = useState(null);
const location = useLocation();
+ const navigate = useNavigate();
+ const { attemptId: urlAttemptId } = useParams<{ attemptId?: string }>();
const { system, profiles } = useUserSystem();
// Memoize latest attempt calculation
@@ -156,39 +158,68 @@ function TaskDetailsToolbar() {
});
if (result.length > 0) {
- // Check if there's an attempt query parameter
+ // Check if we have a new latest attempt (newly created)
+ const currentLatest =
+ taskAttempts.length > 0
+ ? taskAttempts.reduce((latest, current) =>
+ new Date(current.created_at) > new Date(latest.created_at)
+ ? current
+ : latest
+ )
+ : null;
+
+ const newLatest = result.reduce((latest, current) =>
+ new Date(current.created_at) > new Date(latest.created_at)
+ ? current
+ : latest
+ );
+
+ // If we have a new attempt that wasn't there before, navigate to it immediately
+ const hasNewAttempt =
+ newLatest && (!currentLatest || newLatest.id !== currentLatest.id);
+
+ if (hasNewAttempt) {
+ // Always navigate to newly created attempts
+ handleAttemptSelect(newLatest);
+ return;
+ }
+
+ // Otherwise, follow existing logic for URL-based attempt selection
const urlParams = new URLSearchParams(location.search);
- const attemptParam = urlParams.get('attempt');
+ const queryAttemptParam = urlParams.get('attempt');
+ const attemptParam = urlAttemptId || queryAttemptParam;
let selectedAttemptToUse: TaskAttempt;
if (attemptParam) {
- // Try to find the specific attempt
const specificAttempt = result.find(
(attempt) => attempt.id === attemptParam
);
if (specificAttempt) {
selectedAttemptToUse = specificAttempt;
} else {
- // Fall back to latest if specific attempt not found
- selectedAttemptToUse = result.reduce((latest, current) =>
- new Date(current.created_at) > new Date(latest.created_at)
- ? current
- : latest
- );
+ selectedAttemptToUse = newLatest;
}
} else {
- // Use latest attempt if no specific attempt requested
- selectedAttemptToUse = result.reduce((latest, current) =>
- new Date(current.created_at) > new Date(latest.created_at)
- ? current
- : latest
- );
+ selectedAttemptToUse = newLatest;
}
setSelectedAttempt((prev) => {
if (JSON.stringify(prev) === JSON.stringify(selectedAttemptToUse))
return prev;
+
+ // Only navigate if we're not already on the correct attempt URL
+ if (
+ selectedAttemptToUse &&
+ task &&
+ (!urlAttemptId || urlAttemptId !== selectedAttemptToUse.id)
+ ) {
+ navigate(
+ `/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttemptToUse.id}`,
+ { replace: true }
+ );
+ }
+
return selectedAttemptToUse;
});
} else {
@@ -203,7 +234,16 @@ function TaskDetailsToolbar() {
} finally {
setLoading(false);
}
- }, [task, location.search, setLoading, setSelectedAttempt, setAttemptData]);
+ }, [
+ task,
+ location.search,
+ urlAttemptId,
+ navigate,
+ projectId,
+ setLoading,
+ setSelectedAttempt,
+ setAttemptData,
+ ]);
useEffect(() => {
fetchTaskAttempts();
@@ -214,6 +254,20 @@ function TaskDetailsToolbar() {
dispatch({ type: 'ENTER_CREATE_MODE' });
}, []);
+ // Handle attempt selection with URL navigation
+ const handleAttemptSelect = useCallback(
+ (attempt: TaskAttempt | null) => {
+ setSelectedAttempt(attempt);
+ if (attempt && task) {
+ navigate(
+ `/projects/${projectId}/tasks/${task.id}/attempts/${attempt.id}`,
+ { replace: true }
+ );
+ }
+ },
+ [navigate, projectId, task, setSelectedAttempt]
+ );
+
// Stub handlers for backward compatibility with CreateAttempt
const setCreateAttemptBranch = useCallback(
(branch: string | null | ((prev: string | null) => string | null)) => {
@@ -303,6 +357,7 @@ function TaskDetailsToolbar() {
setShowCreatePRDialog={setShowCreatePRDialog}
creatingPR={ui.creatingPR}
handleEnterCreateAttemptMode={handleEnterCreateAttemptMode}
+ handleAttemptSelect={handleAttemptSelect}
branches={branches}
/>
) : (
diff --git a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx
index e2f5c9d4..75494293 100644
--- a/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx
+++ b/frontend/src/components/tasks/Toolbar/CurrentAttempt.tsx
@@ -48,7 +48,6 @@ import {
TaskAttemptDataContext,
TaskAttemptStoppingContext,
TaskDetailsContext,
- TaskSelectedAttemptContext,
} from '@/components/context/taskDetailsContext.ts';
import { useConfig } from '@/components/config-provider.tsx';
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts.ts';
@@ -81,6 +80,7 @@ type Props = {
taskAttempts: TaskAttempt[];
creatingPR: boolean;
handleEnterCreateAttemptMode: () => void;
+ handleAttemptSelect: (attempt: TaskAttempt) => void;
branches: GitBranch[];
};
@@ -92,12 +92,12 @@ function CurrentAttempt({
taskAttempts,
creatingPR,
handleEnterCreateAttemptMode,
+ handleAttemptSelect,
branches,
}: Props) {
const { task, projectId, handleOpenInEditor, projectHasDevScript } =
useContext(TaskDetailsContext);
const { config } = useConfig();
- const { setSelectedAttempt } = useContext(TaskSelectedAttemptContext);
const { isStopping, setIsStopping } = useContext(TaskAttemptStoppingContext);
const { attemptData, fetchAttemptData, isAttemptRunning } = useContext(
TaskAttemptDataContext
@@ -223,10 +223,10 @@ function CurrentAttempt({
const handleAttemptChange = useCallback(
(attempt: TaskAttempt) => {
- setSelectedAttempt(attempt);
+ handleAttemptSelect(attempt);
fetchAttemptData(attempt.id, attempt.task_id);
},
- [fetchAttemptData, setSelectedAttempt]
+ [fetchAttemptData, handleAttemptSelect]
);
const handleMergeClick = async () => {
diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx
index 1c8a33d8..ff71ec67 100644
--- a/frontend/src/pages/project-tasks.tsx
+++ b/frontend/src/pages/project-tasks.tsx
@@ -249,11 +249,14 @@ export function ProjectTasks() {
}, []);
const handleViewTaskDetails = useCallback(
- (task: Task) => {
+ (task: Task, attemptIdToShow?: string) => {
// setSelectedTask(task);
// setIsPanelOpen(true);
- // Update URL to include task ID
- navigate(`/projects/${projectId}/tasks/${task.id}`, { replace: true });
+ // Update URL to include task ID and optionally attempt ID
+ const targetUrl = attemptIdToShow
+ ? `/projects/${projectId}/tasks/${task.id}/attempts/${attemptIdToShow}`
+ : `/projects/${projectId}/tasks/${task.id}`;
+ navigate(targetUrl, { replace: true });
},
[projectId, navigate]
);
@@ -311,7 +314,7 @@ export function ProjectTasks() {
// Setup keyboard shortcuts
useKeyboardShortcuts({
navigate,
- currentPath: `/projects/${projectId}/tasks`,
+ currentPath: window.location.pathname,
hasOpenDialog:
isTaskDialogOpen || isTemplateManagerOpen || isProjectSettingsOpen,
closeDialog: () => setIsTaskDialogOpen(false),