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:
Louis Knight-Webb
2025-08-13 18:10:19 +01:00
committed by GitHub
parent e970a6eb75
commit 141e1686fd
13 changed files with 319 additions and 68 deletions

View File

@@ -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 />}

View File

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

View File

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

View File

@@ -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}

View File

@@ -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">

View 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>
);
}