Add task attempt ID to URL (vibe-kanban) (#463)

* Commit changes from coding agent for task attempt 2a74dbe9-84df-42c8-990e-bd12ad882576

* Commit changes from coding agent for task attempt 2a74dbe9-84df-42c8-990e-bd12ad882576

* Cleanup script changes for task attempt 2a74dbe9-84df-42c8-990e-bd12ad882576

* Commit changes from coding agent for task attempt 2a74dbe9-84df-42c8-990e-bd12ad882576

* Cleanup script changes for task attempt 2a74dbe9-84df-42c8-990e-bd12ad882576
This commit is contained in:
Louis Knight-Webb
2025-08-13 15:26:42 +01:00
committed by GitHub
parent 60e80732cd
commit faa177fe60
4 changed files with 87 additions and 25 deletions

View File

@@ -161,6 +161,10 @@ function AppContent() {
path="/projects/:projectId/tasks"
element={<ProjectTasks />}
/>
<Route
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
element={<ProjectTasks />}
/>
<Route
path="/projects/:projectId/tasks/:taskId"
element={<ProjectTasks />}

View File

@@ -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<string | null>(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}
/>
) : (

View File

@@ -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 () => {

View File

@@ -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),