From 354234b70be04f2e15fa66d40a5172f5a8f48c9a Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sat, 21 Jun 2025 19:13:42 +0100 Subject: [PATCH] Improve layout --- frontend/package-lock.json | 35 ++ frontend/package.json | 1 + .../src/components/tasks/TaskDetailsPanel.tsx | 433 +++++++----------- frontend/src/components/ui/tooltip.tsx | 28 ++ 4 files changed, 218 insertions(+), 279 deletions(-) create mode 100644 frontend/src/components/ui/tooltip.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed386d96..f1ca2f18 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.7", "class-variance-authority": "^0.7.0", "click-to-react-component": "^1.1.2", "clsx": "^2.0.0", @@ -1687,6 +1688,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9cb94dd3..f2a20e60 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.7", "class-variance-authority": "^0.7.0", "click-to-react-component": "^1.1.2", "clsx": "^2.0.0", diff --git a/frontend/src/components/tasks/TaskDetailsPanel.tsx b/frontend/src/components/tasks/TaskDetailsPanel.tsx index 26c267b5..38d8ebfe 100644 --- a/frontend/src/components/tasks/TaskDetailsPanel.tsx +++ b/frontend/src/components/tasks/TaskDetailsPanel.tsx @@ -6,29 +6,39 @@ import { Clock, FileText, Code, + ChevronDown, + ChevronUp, + Plus, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; + import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { makeRequest } from "@/lib/api"; -import { getTaskPanelClasses, getBackdropClasses } from "@/lib/responsive-config"; +import { + getTaskPanelClasses, + getBackdropClasses, +} from "@/lib/responsive-config"; import type { TaskStatus, TaskAttempt, TaskAttemptActivity, TaskAttemptStatus, - ExecutionProcess, - ExecutionProcessStatus, - ExecutionProcessType, ApiResponse, TaskWithAttemptStatus, } from "shared/types"; @@ -100,51 +110,6 @@ const getAttemptStatusDisplay = ( } }; -const getProcessStatusDisplay = ( - status: ExecutionProcessStatus -): { label: string; className: string } => { - switch (status) { - case "running": - return { - label: "Running", - className: "bg-status-running text-status-running-foreground", - }; - case "completed": - return { - label: "Completed", - className: "bg-status-complete text-status-complete-foreground", - }; - case "failed": - return { - label: "Failed", - className: "bg-status-failed text-status-failed-foreground", - }; - case "killed": - return { - label: "Killed", - className: "bg-status-failed text-status-failed-foreground", - }; - default: - return { - label: "Unknown", - className: "bg-status-init text-status-init-foreground", - }; - } -}; - -const getProcessTypeDisplay = (type: ExecutionProcessType): string => { - switch (type) { - case "setupscript": - return "Setup Script"; - case "codingagent": - return "Coding Agent"; - case "devserver": - return "Dev Server"; - default: - return "Unknown"; - } -}; - export function TaskDetailsPanel({ task, projectId, @@ -158,12 +123,9 @@ export function TaskDetailsPanel({ const [attemptActivities, setAttemptActivities] = useState< TaskAttemptActivity[] >([]); - const [executionProcesses, setExecutionProcesses] = useState< - ExecutionProcess[] - >([]); const [loading, setLoading] = useState(false); const [followUpMessage, setFollowUpMessage] = useState(""); - const [showAttemptHistory, setShowAttemptHistory] = useState(false); + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); // Check if the selected attempt is active (not in a final state) const isAttemptRunning = @@ -181,7 +143,6 @@ export function TaskDetailsPanel({ const interval = setInterval(() => { if (selectedAttempt) { fetchAttemptActivities(selectedAttempt.id, true); - fetchExecutionProcesses(selectedAttempt.id, true); } }, 2000); @@ -217,7 +178,6 @@ export function TaskDetailsPanel({ ); setSelectedAttempt(latestAttempt); fetchAttemptActivities(latestAttempt.id); - fetchExecutionProcesses(latestAttempt.id); } } } @@ -251,35 +211,11 @@ export function TaskDetailsPanel({ } }; - const fetchExecutionProcesses = async ( - attemptId: string, - _isBackgroundUpdate = false - ) => { - if (!task) return; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts/${attemptId}/execution-processes` - ); - - if (response.ok) { - const result: ApiResponse = await response.json(); - if (result.success && result.data) { - setExecutionProcesses(result.data); - } - } - } catch (err) { - console.error("Failed to fetch execution processes:", err); - } - }; - const handleAttemptChange = (attemptId: string) => { const attempt = taskAttempts.find((a) => a.id === attemptId); if (attempt) { setSelectedAttempt(attempt); fetchAttemptActivities(attempt.id); - fetchExecutionProcesses(attempt.id); - setShowAttemptHistory(false); } }; @@ -289,30 +225,6 @@ export function TaskDetailsPanel({ setFollowUpMessage(""); }; - const stopExecutionProcess = async (processId: string) => { - if (!task || !selectedAttempt) return; - - try { - const response = await makeRequest( - `/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/execution-processes/${processId}/stop`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - } - ); - - if (response.ok) { - // Refresh the execution processes - fetchExecutionProcesses(selectedAttempt.id); - fetchAttemptActivities(selectedAttempt.id); - } - } catch (err) { - console.error("Failed to stop execution process:", err); - } - }; - const openInEditor = async () => { if (!task || !selectedAttempt) return; @@ -331,6 +243,29 @@ export function TaskDetailsPanel({ } }; + const createNewAttempt = async () => { + if (!task) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/tasks/${task.id}/attempts`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (response.ok) { + // Refresh the attempts list + fetchTaskAttempts(); + } + } catch (err) { + console.error("Failed to create new attempt:", err); + } + }; + if (!task) return null; return ( @@ -338,15 +273,10 @@ export function TaskDetailsPanel({ {isOpen && ( <> {/* Backdrop - only on smaller screens (overlay mode) */} -
+
{/* Panel */} -
+
{/* Header */}
@@ -380,13 +310,59 @@ export function TaskDetailsPanel({
+ {/* Description */} +
+
+ {task.description ? ( +
+

200 + ? "line-clamp-3" + : "" + }`} + > + {task.description} +

+ {task.description.length > 200 && ( + + )} +
+ ) : ( +

+ No description provided +

+ )} +
+
+ {/* Attempt Selection */} -
- {selectedAttempt && !showAttemptHistory ? ( -
- - Current attempt: - +
+
+ + Current attempt: + + {selectedAttempt && ( {new Date( selectedAttempt.created_at @@ -395,55 +371,64 @@ export function TaskDetailsPanel({ selectedAttempt.created_at ).toLocaleTimeString()} + )} +
{taskAttempts.length > 1 && ( - + + + + + + {taskAttempts.map((attempt) => ( + handleAttemptChange(attempt.id)} + className={ + selectedAttempt?.id === attempt.id + ? "bg-accent" + : "" + } + > +
+ + {new Date( + attempt.created_at + ).toLocaleDateString()}{" "} + {new Date( + attempt.created_at + ).toLocaleTimeString()} + + + {attempt.executor || "executor"} + +
+
+ ))} +
+
)} + + + + + + +

Create new attempt

+
+
+
- ) : ( -
- - -
- )} +
{selectedAttempt && (
@@ -482,116 +467,6 @@ export function TaskDetailsPanel({
) : ( <> - {/* Description */} -
- -
- {task.description ? ( -

- {task.description} -

- ) : ( -

- No description provided -

- )} -
-
- - {/* Execution Processes */} - {selectedAttempt && executionProcesses.length > 0 && ( -
- -
- {executionProcesses.map((process) => ( - - -
-
- - { - getProcessStatusDisplay(process.status) - .label - } - - - {getProcessTypeDisplay( - process.process_type - )} - -
-
- - {new Date( - process.started_at - ).toLocaleTimeString()} - - {process.status === "running" && ( - - )} -
-
- - {(process.stdout || process.stderr) && ( -
- {process.stdout && ( -
- -
- {process.stdout} -
-
- )} - {process.stderr && ( -
- -
- {process.stderr} -
-
- )} -
- )} -
-
- ))} -
-
- )} - {/* Activity History */} {selectedAttempt && (
@@ -604,7 +479,7 @@ export function TaskDetailsPanel({
) : (
- {attemptActivities.map((activity) => ( + {attemptActivities.slice().reverse().map((activity) => (
diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 00000000..60d9678c --- /dev/null +++ b/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }