VS Code companion (#461)
* Init port discovery * Fmt * Remove unused * Fmt * Simplify * Container lookup API * Isolated task details * Fmt * Lint and format * Lint
This commit is contained in:
committed by
GitHub
parent
e970a6eb75
commit
141e1686fd
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
import { BrowserRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { Navbar } from '@/components/layout/navbar';
|
||||
import { Projects } from '@/pages/projects';
|
||||
import { ProjectTasks } from '@/pages/project-tasks';
|
||||
import { TaskDetailsPage } from '@/pages/task-details';
|
||||
|
||||
import { Settings } from '@/pages/Settings';
|
||||
import { McpServers } from '@/pages/McpServers';
|
||||
@@ -22,11 +23,12 @@ const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
|
||||
function AppContent() {
|
||||
const { config, updateConfig, loading } = useConfig();
|
||||
const location = useLocation();
|
||||
const [showDisclaimer, setShowDisclaimer] = useState(false);
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [showPrivacyOptIn, setShowPrivacyOptIn] = useState(false);
|
||||
const [showGitHubLogin, setShowGitHubLogin] = useState(false);
|
||||
const showNavbar = true;
|
||||
const showNavbar = !location.pathname.endsWith('/full');
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
@@ -161,6 +163,14 @@ function AppContent() {
|
||||
path="/projects/:projectId/tasks"
|
||||
element={<ProjectTasks />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId/full"
|
||||
element={<TaskDetailsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/projects/:projectId/tasks/:taskId/attempts/:attemptId"
|
||||
element={<ProjectTasks />}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import TaskDetailsToolbar from '@/components/tasks/TaskDetailsToolbar.tsx';
|
||||
|
||||
function CollapsibleToolbar() {
|
||||
const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div className="px-4 pb-2 flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Task Details
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsHeaderCollapsed((prev) => !prev)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
{isHeaderCollapsed ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{!isHeaderCollapsed && <TaskDetailsToolbar />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(CollapsibleToolbar);
|
||||
@@ -15,6 +15,7 @@ interface TaskDetailsHeaderProps {
|
||||
onClose: () => void;
|
||||
onEditTask?: (task: TaskWithAttemptStatus) => void;
|
||||
onDeleteTask?: (taskId: string) => void;
|
||||
hideCloseButton?: boolean;
|
||||
}
|
||||
|
||||
const statusLabels: Record<TaskStatus, string> = {
|
||||
@@ -46,6 +47,7 @@ function TaskDetailsHeader({
|
||||
onClose,
|
||||
onEditTask,
|
||||
onDeleteTask,
|
||||
hideCloseButton = false,
|
||||
}: TaskDetailsHeaderProps) {
|
||||
const { task } = useContext(TaskDetailsContext);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
@@ -102,18 +104,20 @@ function TaskDetailsHeader({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Close panel</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{!hideCloseButton && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Close panel</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import LogsTab from '@/components/tasks/TaskDetails/LogsTab.tsx';
|
||||
import ProcessesTab from '@/components/tasks/TaskDetails/ProcessesTab.tsx';
|
||||
import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmationDialog.tsx';
|
||||
import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx';
|
||||
import CollapsibleToolbar from '@/components/tasks/TaskDetails/CollapsibleToolbar.tsx';
|
||||
import TaskDetailsProvider from '../context/TaskDetailsContextProvider.tsx';
|
||||
import TaskDetailsToolbar from './TaskDetailsToolbar.tsx';
|
||||
|
||||
interface TaskDetailsPanelProps {
|
||||
task: TaskWithAttemptStatus | null;
|
||||
@@ -24,6 +24,9 @@ interface TaskDetailsPanelProps {
|
||||
onEditTask?: (task: TaskWithAttemptStatus) => void;
|
||||
onDeleteTask?: (taskId: string) => void;
|
||||
isDialogOpen?: boolean;
|
||||
hideBackdrop?: boolean;
|
||||
className?: string;
|
||||
hideHeader?: boolean;
|
||||
}
|
||||
|
||||
export function TaskDetailsPanel({
|
||||
@@ -34,6 +37,9 @@ export function TaskDetailsPanel({
|
||||
onEditTask,
|
||||
onDeleteTask,
|
||||
isDialogOpen = false,
|
||||
hideBackdrop = false,
|
||||
className,
|
||||
hideHeader = false,
|
||||
}: TaskDetailsPanelProps) {
|
||||
const [showEditorDialog, setShowEditorDialog] = useState(false);
|
||||
|
||||
@@ -74,18 +80,23 @@ export function TaskDetailsPanel({
|
||||
projectHasDevScript={projectHasDevScript}
|
||||
>
|
||||
{/* Backdrop - only on smaller screens (overlay mode) */}
|
||||
<div className={getBackdropClasses()} onClick={onClose} />
|
||||
{!hideBackdrop && (
|
||||
<div className={getBackdropClasses()} onClick={onClose} />
|
||||
)}
|
||||
|
||||
{/* Panel */}
|
||||
<div className={getTaskPanelClasses()}>
|
||||
<div className={className || getTaskPanelClasses()}>
|
||||
<div className="flex flex-col h-full">
|
||||
<TaskDetailsHeader
|
||||
onClose={onClose}
|
||||
onEditTask={onEditTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
/>
|
||||
{!hideHeader && (
|
||||
<TaskDetailsHeader
|
||||
onClose={onClose}
|
||||
onEditTask={onEditTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
hideCloseButton={hideBackdrop}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CollapsibleToolbar />
|
||||
<TaskDetailsToolbar />
|
||||
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
|
||||
@@ -78,6 +78,7 @@ function TaskDetailsToolbar() {
|
||||
);
|
||||
|
||||
const { isStopping } = useContext(TaskAttemptStoppingContext);
|
||||
const location = useLocation();
|
||||
const { setAttemptData, isAttemptRunning } = useContext(
|
||||
TaskAttemptDataContext
|
||||
);
|
||||
@@ -91,7 +92,6 @@ function TaskDetailsToolbar() {
|
||||
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
|
||||
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { attemptId: urlAttemptId } = useParams<{ attemptId?: string }>();
|
||||
const { system, profiles } = useUserSystem();
|
||||
@@ -214,10 +214,11 @@ function TaskDetailsToolbar() {
|
||||
task &&
|
||||
(!urlAttemptId || urlAttemptId !== selectedAttemptToUse.id)
|
||||
) {
|
||||
navigate(
|
||||
`/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttemptToUse.id}`,
|
||||
{ replace: true }
|
||||
);
|
||||
const isFullScreen = location.pathname.endsWith('/full');
|
||||
const targetUrl = isFullScreen
|
||||
? `/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttemptToUse.id}/full`
|
||||
: `/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttemptToUse.id}`;
|
||||
navigate(targetUrl, { replace: true });
|
||||
}
|
||||
|
||||
return selectedAttemptToUse;
|
||||
@@ -259,13 +260,14 @@ function TaskDetailsToolbar() {
|
||||
(attempt: TaskAttempt | null) => {
|
||||
setSelectedAttempt(attempt);
|
||||
if (attempt && task) {
|
||||
navigate(
|
||||
`/projects/${projectId}/tasks/${task.id}/attempts/${attempt.id}`,
|
||||
{ replace: true }
|
||||
);
|
||||
const isFullScreen = location.pathname.endsWith('/full');
|
||||
const targetUrl = isFullScreen
|
||||
? `/projects/${projectId}/tasks/${task.id}/attempts/${attempt.id}/full`
|
||||
: `/projects/${projectId}/tasks/${task.id}/attempts/${attempt.id}`;
|
||||
navigate(targetUrl, { replace: true });
|
||||
}
|
||||
},
|
||||
[navigate, projectId, task, setSelectedAttempt]
|
||||
[navigate, projectId, task, setSelectedAttempt, location.pathname]
|
||||
);
|
||||
|
||||
// Stub handlers for backward compatibility with CreateAttempt
|
||||
@@ -323,7 +325,7 @@ function TaskDetailsToolbar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 pb-4 border-b">
|
||||
<div className="p-4 border-b">
|
||||
{/* Error Display */}
|
||||
{ui.error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
|
||||
131
frontend/src/pages/task-details.tsx
Normal file
131
frontend/src/pages/task-details.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import { TaskDetailsPanel } from '@/components/tasks/TaskDetailsPanel';
|
||||
import { projectsApi, tasksApi } from '@/lib/api';
|
||||
import type { TaskWithAttemptStatus, Project } from 'shared/types';
|
||||
|
||||
export function TaskDetailsPage() {
|
||||
const { projectId, taskId, attemptId } = useParams<{
|
||||
projectId: string;
|
||||
taskId: string;
|
||||
attemptId?: string;
|
||||
}>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [task, setTask] = useState<TaskWithAttemptStatus | null>(null);
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
navigate(`/projects/${projectId}/tasks`, { replace: true });
|
||||
};
|
||||
|
||||
const handleEditTask = (task: TaskWithAttemptStatus) => {
|
||||
// Navigate back to main task page and trigger edit
|
||||
navigate(`/projects/${projectId}/tasks/${task.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteTask = () => {
|
||||
// Navigate back to main task page after deletion
|
||||
// navigate(`/projects/${projectId}/tasks`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!projectId || !taskId) {
|
||||
setError('Missing project or task ID');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Fetch both project and tasks in parallel
|
||||
const [projectResult, tasksResult] = await Promise.all([
|
||||
projectsApi.getById(projectId),
|
||||
tasksApi.getAll(projectId),
|
||||
]);
|
||||
|
||||
// Find the specific task from the list (to get TaskWithAttemptStatus)
|
||||
const foundTask = tasksResult.find((t) => t.id === taskId);
|
||||
|
||||
if (!foundTask) {
|
||||
setError('Task not found');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setProject(projectResult);
|
||||
setTask(foundTask);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch task details:', err);
|
||||
setError('Failed to load task details');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [projectId, taskId, attemptId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<Loader message="Loading task details..." size={32} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-destructive text-lg mb-4">{error}</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Back to tasks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task || !project) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-muted-foreground text-lg mb-4">
|
||||
Task not found
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Back to tasks
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<TaskDetailsPanel
|
||||
task={task}
|
||||
projectHasDevScript={!!project.dev_script}
|
||||
projectId={projectId!}
|
||||
onClose={handleClose}
|
||||
onEditTask={handleEditTask}
|
||||
onDeleteTask={handleDeleteTask}
|
||||
hideBackdrop={true}
|
||||
hideHeader={true}
|
||||
className="w-full h-screen flex flex-col"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user