feat: full screen task attempt view (#553)
* full screen task attempt view * fix tsc errors * remove padding * Full screen view improvements (#568) * delete FullscreenHeaderContext.tsx * add fullscreen responsive layout * full screen props * cleanup fullscreen * fmt * task status * task actions * simplify toolbar * reset CurrentAttempt * buttons * Truncate properly branch name * fmt * polish * fmt --------- Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
committed by
GitHub
parent
3d6013ba32
commit
37e401fb0f
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -50,6 +50,7 @@
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
@@ -2921,6 +2922,16 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/container-queries": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
|
||||
"integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||
|
||||
@@ -51,11 +51,12 @@
|
||||
"react-virtuoso": "^4.13.0",
|
||||
"react-window": "^1.8.11",
|
||||
"rfc6902": "^5.1.2",
|
||||
"zustand": "^4.5.4",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
@@ -74,4 +75,4 @@
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Circle,
|
||||
CircleCheck,
|
||||
CircleDotDashed,
|
||||
} from 'lucide-react';
|
||||
import type { TodoItem } from 'shared/types';
|
||||
|
||||
interface PinnedTodoBoxProps {
|
||||
todos: TodoItem[];
|
||||
lastUpdated: string | null;
|
||||
}
|
||||
|
||||
const getStatusIcon = (status: string): React.ReactNode => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'completed':
|
||||
return <CircleCheck className="h-4 w-4 text-green-500" />;
|
||||
case 'in_progress':
|
||||
case 'in-progress':
|
||||
return <CircleDotDashed className="h-4 w-4 text-blue-500" />;
|
||||
case 'pending':
|
||||
case 'todo':
|
||||
return <Circle className="h-4 w-4 text-gray-400" />;
|
||||
default:
|
||||
return <Circle className="h-4 w-4 text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
export const PinnedTodoBox: React.FC<PinnedTodoBoxProps> = ({ todos }) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
if (todos.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 border-b bg-muted/20">
|
||||
{isCollapsed && (
|
||||
<div
|
||||
className="flex items-center justify-between px-4 py-2 cursor-pointer"
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleCheck className="h-4 w-4 text-primary" />
|
||||
<span className="text-sm text-primary">TODOs</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDown className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="relative">
|
||||
<div className="absolute top-2 right-2 z-20">
|
||||
<button
|
||||
className="flex items-center justify-center p-1 cursor-pointer hover:bg-muted/40 rounded"
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4 text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 py-2 pr-10 space-y-2 max-h-64 overflow-y-auto">
|
||||
{todos.map((todo, index) => (
|
||||
<div
|
||||
key={`${todo.content}-${index}`}
|
||||
className="flex items-start gap-2 text-sm"
|
||||
>
|
||||
<span className="mt-0.5 flex-shrink-0">
|
||||
{getStatusIcon(todo.status)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="break-words text-primary">
|
||||
{todo.content}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -112,7 +112,7 @@ function BranchSelector({
|
||||
size="sm"
|
||||
className={`w-full justify-between text-xs ${className}`}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 w-full">
|
||||
<GitBranchIcon className="h-3 w-3" />
|
||||
<span className="truncate">{displayName}</span>
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
TaskSelectedAttemptContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useProcessesLogs } from '@/hooks/useProcessesLogs';
|
||||
import { usePinnedTodos } from '@/hooks/usePinnedTodos';
|
||||
import LogEntryRow from '@/components/logs/LogEntryRow';
|
||||
import { PinnedTodoBox } from '@/components/PinnedTodoBox';
|
||||
import {
|
||||
shouldShowInLogs,
|
||||
isAutoCollapsibleProcess,
|
||||
@@ -139,9 +137,6 @@ function LogsTab() {
|
||||
|
||||
const { entries } = useProcessesLogs(filteredProcesses, true);
|
||||
|
||||
// Extract todos from entries using the usePinnedTodos hook
|
||||
const { todos, lastUpdated } = usePinnedTodos(entries);
|
||||
|
||||
// Combined collapsed processes (auto + user)
|
||||
const allCollapsedProcesses = useMemo(() => {
|
||||
const combined = new Set(state.autoCollapsed);
|
||||
@@ -282,7 +277,6 @@ function LogsTab() {
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<PinnedTodoBox todos={todos} lastUpdated={lastUpdated} />
|
||||
<div className="flex-1">
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
|
||||
@@ -6,13 +6,14 @@ import type { TabType } from '@/types/tabs';
|
||||
type Props = {
|
||||
activeTab: TabType;
|
||||
setActiveTab: (tab: TabType) => void;
|
||||
rightContent?: React.ReactNode;
|
||||
};
|
||||
|
||||
function TabNavigation({ activeTab, setActiveTab }: Props) {
|
||||
function TabNavigation({ activeTab, setActiveTab, rightContent }: Props) {
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
return (
|
||||
<div className="border-b bg-muted/20">
|
||||
<div className="flex px-4">
|
||||
<div className="border-b bg-muted/20 sticky top-0 z-10">
|
||||
<div className="flex items-center px-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('logs');
|
||||
@@ -58,6 +59,7 @@ function TabNavigation({ activeTab, setActiveTab }: Props) {
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<div className="ml-auto flex items-center">{rightContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { memo, useContext, useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, Edit, Trash2, X } from 'lucide-react';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Chip } from '@/components/ui/chip';
|
||||
import {
|
||||
@@ -16,6 +24,8 @@ interface TaskDetailsHeaderProps {
|
||||
onEditTask?: (task: TaskWithAttemptStatus) => void;
|
||||
onDeleteTask?: (taskId: string) => void;
|
||||
hideCloseButton?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
setFullScreen?: (isFullScreen: boolean) => void;
|
||||
}
|
||||
|
||||
const statusLabels: Record<TaskStatus, string> = {
|
||||
@@ -48,6 +58,8 @@ function TaskDetailsHeader({
|
||||
onEditTask,
|
||||
onDeleteTask,
|
||||
hideCloseButton = false,
|
||||
isFullScreen,
|
||||
setFullScreen,
|
||||
}: TaskDetailsHeaderProps) {
|
||||
const { task } = useContext(TaskDetailsContext);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
@@ -55,116 +67,153 @@ function TaskDetailsHeader({
|
||||
return (
|
||||
<div>
|
||||
{/* Title and Task Actions */}
|
||||
<div className="p-4 pb-2">
|
||||
<div className="p-4 pb-2 border-b-2 border-muted">
|
||||
{/* Top row: title and action icons */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-bold mb-1 line-clamp-2">
|
||||
{task.title}
|
||||
</h2>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Chip dotColor={getTaskStatusDotColor(task.status)}>
|
||||
{statusLabels[task.status]}
|
||||
</Chip>
|
||||
<div className="flex-1 min-w-0 flex items-start gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-lg font-bold mb-1 line-clamp-2">
|
||||
{task.title}
|
||||
<Chip
|
||||
className="ml-2 -mt-2 relative top-[-2px]"
|
||||
dotColor={getTaskStatusDotColor(task.status)}
|
||||
>
|
||||
{statusLabels[task.status]}
|
||||
</Chip>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{onEditTask && (
|
||||
{setFullScreen && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEditTask(task)}
|
||||
onClick={() => setFullScreen(!isFullScreen)}
|
||||
aria-label={
|
||||
isFullScreen
|
||||
? 'Collapse to sidebar'
|
||||
: 'Expand to fullscreen'
|
||||
}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
{isFullScreen ? (
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
) : (
|
||||
<Maximize2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit task</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{onDeleteTask && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDeleteTask(task.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete task</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>
|
||||
<p>
|
||||
{isFullScreen
|
||||
? 'Collapse to sidebar'
|
||||
: 'Expand to fullscreen'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
{onEditTask && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEditTask(task)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit task</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
{onDeleteTask && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDeleteTask(task.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete task</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>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="mt-2">
|
||||
<div className="p-2 bg-muted/20 rounded border-l-2 border-muted max-h-48 overflow-y-auto">
|
||||
{task.description ? (
|
||||
<div>
|
||||
<p
|
||||
className={`text-xs whitespace-pre-wrap text-muted-foreground ${
|
||||
!isDescriptionExpanded && task.description.length > 150
|
||||
? 'line-clamp-3'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
{task.description.length > 150 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded)
|
||||
}
|
||||
className="mt-1 p-0 h-auto text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{isDescriptionExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3 mr-1" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3 mr-1" />
|
||||
Show more
|
||||
</>
|
||||
{/* Description + Status (sidebar view only) lives below icons */}
|
||||
{!isFullScreen && (
|
||||
<div className="mt-2">
|
||||
<div className="p-2 bg-muted/20 rounded border-l-2 border-muted max-h-48 overflow-y-auto">
|
||||
<div className="flex items-start gap-2 text-xs text-muted-foreground">
|
||||
{task.description ? (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p
|
||||
className={`whitespace-pre-wrap ${
|
||||
!isDescriptionExpanded && task.description.length > 150
|
||||
? 'line-clamp-3'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
{task.description.length > 150 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded)
|
||||
}
|
||||
className="mt-1 p-0 h-auto text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{isDescriptionExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3 mr-1" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3 mr-1" />
|
||||
Show more
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="italic">No description provided</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground italic">
|
||||
No description provided
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,11 +12,15 @@ import DiffTab from '@/components/tasks/TaskDetails/DiffTab.tsx';
|
||||
import LogsTab from '@/components/tasks/TaskDetails/LogsTab.tsx';
|
||||
import ProcessesTab from '@/components/tasks/TaskDetails/ProcessesTab.tsx';
|
||||
import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmationDialog.tsx';
|
||||
import CreatePRDialog from '@/components/tasks/Toolbar/CreatePRDialog';
|
||||
import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx';
|
||||
import TaskDetailsProvider from '../context/TaskDetailsContextProvider.tsx';
|
||||
import TaskDetailsToolbar from './TaskDetailsToolbar.tsx';
|
||||
import TodoPanel from '@/components/tasks/TodoPanel';
|
||||
import { TabNavContext } from '@/contexts/TabNavigationContext';
|
||||
import { ProcessSelectionProvider } from '@/contexts/ProcessSelectionContext';
|
||||
import { projectsApi } from '@/lib/api';
|
||||
import type { GitBranch } from 'shared/types';
|
||||
|
||||
interface TaskDetailsPanelProps {
|
||||
task: TaskWithAttemptStatus | null;
|
||||
@@ -29,6 +33,11 @@ interface TaskDetailsPanelProps {
|
||||
hideBackdrop?: boolean;
|
||||
className?: string;
|
||||
hideHeader?: boolean;
|
||||
isFullScreen?: boolean;
|
||||
setFullScreen?: (value: boolean) => void;
|
||||
forceCreateAttempt?: boolean;
|
||||
onLeaveForceCreateAttempt?: () => void;
|
||||
onNewAttempt?: () => void;
|
||||
}
|
||||
|
||||
export function TaskDetailsPanel({
|
||||
@@ -41,9 +50,16 @@ export function TaskDetailsPanel({
|
||||
isDialogOpen = false,
|
||||
hideBackdrop = false,
|
||||
className,
|
||||
hideHeader = false,
|
||||
isFullScreen,
|
||||
setFullScreen,
|
||||
forceCreateAttempt,
|
||||
onLeaveForceCreateAttempt,
|
||||
}: TaskDetailsPanelProps) {
|
||||
const [showEditorDialog, setShowEditorDialog] = useState(false);
|
||||
const [showCreatePRDialog, setShowCreatePRDialog] = useState(false);
|
||||
const [creatingPR, setCreatingPR] = useState(false);
|
||||
const [, setPrError] = useState<string | null>(null);
|
||||
const [branches, setBranches] = useState<GitBranch[]>([]);
|
||||
|
||||
// Tab and collapsible state
|
||||
const [activeTab, setActiveTab] = useState<TabType>('logs');
|
||||
@@ -55,6 +71,19 @@ export function TaskDetailsPanel({
|
||||
}
|
||||
}, [task?.id]);
|
||||
|
||||
// Fetch branches for PR dialog usage when panel opens
|
||||
useEffect(() => {
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const result = await projectsApi.getBranches(projectId);
|
||||
setBranches(result);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
if (projectId) fetchBranches();
|
||||
}, [projectId]);
|
||||
|
||||
// Handle ESC key locally to prevent global navigation
|
||||
useEffect(() => {
|
||||
if (isDialogOpen) return;
|
||||
@@ -85,40 +114,102 @@ export function TaskDetailsPanel({
|
||||
<ProcessSelectionProvider>
|
||||
{/* Backdrop - only on smaller screens (overlay mode) */}
|
||||
{!hideBackdrop && (
|
||||
<div className={getBackdropClasses()} onClick={onClose} />
|
||||
<div
|
||||
className={getBackdropClasses(isFullScreen || false)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Panel */}
|
||||
<div className={className || getTaskPanelClasses()}>
|
||||
<div
|
||||
className={
|
||||
className || getTaskPanelClasses(isFullScreen || false)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col h-full">
|
||||
{!hideHeader && (
|
||||
<TaskDetailsHeader
|
||||
onClose={onClose}
|
||||
onEditTask={onEditTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
hideCloseButton={hideBackdrop}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TaskDetailsToolbar />
|
||||
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
<TaskDetailsHeader
|
||||
onClose={onClose}
|
||||
onEditTask={onEditTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
hideCloseButton={hideBackdrop}
|
||||
isFullScreen={isFullScreen}
|
||||
setFullScreen={setFullScreen}
|
||||
/>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{activeTab === 'diffs' ? (
|
||||
<DiffTab />
|
||||
) : activeTab === 'processes' ? (
|
||||
<ProcessesTab />
|
||||
) : (
|
||||
<LogsTab />
|
||||
)}
|
||||
</div>
|
||||
{isFullScreen ? (
|
||||
<div className="flex-1 min-h-0 flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-[28rem] shrink-0 border-r overflow-y-auto p-4 space-y-4">
|
||||
{/* Fullscreen sidebar shows description only (no title) above edit/delete */}
|
||||
<div className="space-y-2">
|
||||
{/* Description */}
|
||||
<div className="text-sm text-muted-foreground block">
|
||||
{task.description ? (
|
||||
<p className="whitespace-pre-wrap break-words">
|
||||
{task.description}
|
||||
</p>
|
||||
) : (
|
||||
<p className="italic">No description provided</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TaskFollowUpSection />
|
||||
{/* Current Attempt / Actions */}
|
||||
<TaskDetailsToolbar
|
||||
forceCreateAttempt={forceCreateAttempt}
|
||||
onLeaveForceCreateAttempt={onLeaveForceCreateAttempt}
|
||||
// hide actions in sidebar; moved to header in fullscreen
|
||||
/>
|
||||
|
||||
{/* Task Breakdown (TODOs) */}
|
||||
<TodoPanel />
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-h-0 flex flex-col">
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{activeTab === 'diffs' ? (
|
||||
<DiffTab />
|
||||
) : activeTab === 'processes' ? (
|
||||
<ProcessesTab />
|
||||
) : (
|
||||
<LogsTab />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TaskFollowUpSection />
|
||||
</main>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="p-4 border-b">
|
||||
<TaskDetailsToolbar />
|
||||
</div>
|
||||
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{activeTab === 'diffs' ? (
|
||||
<DiffTab />
|
||||
) : activeTab === 'processes' ? (
|
||||
<ProcessesTab />
|
||||
) : (
|
||||
<LogsTab />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TaskFollowUpSection />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,6 +221,15 @@ export function TaskDetailsPanel({
|
||||
<DeleteFileConfirmationDialog />
|
||||
</ProcessSelectionProvider>
|
||||
</TabNavContext.Provider>
|
||||
{/* PR Dialog mounted within provider so it has task context */}
|
||||
<CreatePRDialog
|
||||
creatingPR={creatingPR}
|
||||
setShowCreatePRDialog={setShowCreatePRDialog}
|
||||
showCreatePRDialog={showCreatePRDialog}
|
||||
setCreatingPR={setCreatingPR}
|
||||
setError={setPrError}
|
||||
branches={branches}
|
||||
/>
|
||||
</TaskDetailsProvider>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -70,7 +70,13 @@ function uiReducer(state: UiState, action: UiAction): UiState {
|
||||
}
|
||||
}
|
||||
|
||||
function TaskDetailsToolbar() {
|
||||
function TaskDetailsToolbar({
|
||||
forceCreateAttempt,
|
||||
onLeaveForceCreateAttempt,
|
||||
}: {
|
||||
forceCreateAttempt?: boolean;
|
||||
onLeaveForceCreateAttempt?: () => void;
|
||||
}) {
|
||||
const { task, projectId } = useContext(TaskDetailsContext);
|
||||
const { setLoading } = useContext(TaskAttemptLoadingContext);
|
||||
const { selectedAttempt, setSelectedAttempt } = useContext(
|
||||
@@ -109,7 +115,8 @@ function TaskDetailsToolbar() {
|
||||
|
||||
// Derived state
|
||||
const isInCreateAttemptMode =
|
||||
ui.userForcedCreateMode || taskAttempts.length === 0;
|
||||
forceCreateAttempt ??
|
||||
(ui.userForcedCreateMode || taskAttempts.length === 0);
|
||||
|
||||
// Derive createAttemptBranch for backward compatibility
|
||||
const createAttemptBranch = useMemo(() => {
|
||||
@@ -291,10 +298,11 @@ function TaskDetailsToolbar() {
|
||||
if (boolValue) {
|
||||
dispatch({ type: 'ENTER_CREATE_MODE' });
|
||||
} else {
|
||||
if (onLeaveForceCreateAttempt) onLeaveForceCreateAttempt();
|
||||
dispatch({ type: 'LEAVE_CREATE_MODE' });
|
||||
}
|
||||
},
|
||||
[isInCreateAttemptMode]
|
||||
[isInCreateAttemptMode, onLeaveForceCreateAttempt]
|
||||
);
|
||||
|
||||
// Wrapper functions for UI state dispatch
|
||||
@@ -326,7 +334,7 @@ function TaskDetailsToolbar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-4 border-b">
|
||||
<div>
|
||||
{/* Error Display */}
|
||||
{ui.error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
@@ -375,13 +383,13 @@ function TaskDetailsToolbar() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Special Actions */}
|
||||
{/* Special Actions: show only in sidebar (non-fullscreen) */}
|
||||
{!selectedAttempt && !isAttemptRunning && !isStopping && (
|
||||
<div className="space-y-2 pt-3 border-t">
|
||||
<Button
|
||||
onClick={handleEnterCreateAttemptMode}
|
||||
size="sm"
|
||||
className="w-full gap-2"
|
||||
className="w-full gap-2 bg-black text-white hover:bg-black/90"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
Start Attempt
|
||||
|
||||
58
frontend/src/components/tasks/TodoPanel.tsx
Normal file
58
frontend/src/components/tasks/TodoPanel.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { Circle, CircleCheckBig, CircleDotDashed } from 'lucide-react';
|
||||
import { useProcessesLogs } from '@/hooks/useProcessesLogs';
|
||||
import { usePinnedTodos } from '@/hooks/usePinnedTodos';
|
||||
import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext';
|
||||
import { shouldShowInLogs } from '@/constants/processes';
|
||||
|
||||
function getStatusIcon(status?: string) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (s === 'completed')
|
||||
return <CircleCheckBig aria-hidden className="h-4 w-4 text-green-600" />;
|
||||
if (s === 'in_progress' || s === 'in-progress')
|
||||
return <CircleDotDashed aria-hidden className="h-4 w-4 text-blue-500" />;
|
||||
return <Circle aria-hidden className="h-4 w-4 text-muted-foreground" />;
|
||||
}
|
||||
|
||||
export function TodoPanel() {
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
|
||||
const filteredProcesses = useMemo(
|
||||
() =>
|
||||
(attemptData.processes || []).filter((p) =>
|
||||
shouldShowInLogs(p.run_reason)
|
||||
),
|
||||
[attemptData.processes?.map((p) => p.id).join(',')]
|
||||
);
|
||||
|
||||
const { entries } = useProcessesLogs(filteredProcesses, true);
|
||||
const { todos } = usePinnedTodos(entries);
|
||||
|
||||
// Only show once the agent has created subtasks
|
||||
if (!todos || todos.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-background rounded-lg overflow-hidden border">
|
||||
<div className="p-4">
|
||||
<h3 className="font-medium mb-3">Task Breakdown</h3>
|
||||
<ul className="space-y-2">
|
||||
{todos.map((todo, index) => (
|
||||
<li
|
||||
key={`${todo.content}-${index}`}
|
||||
className="flex items-start gap-2"
|
||||
>
|
||||
<span className="mt-0.5 h-4 w-4 flex items-center justify-center shrink-0">
|
||||
{getStatusIcon(todo.status)}
|
||||
</span>
|
||||
<span className="text-sm leading-5 break-words">
|
||||
{todo.content}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TodoPanel;
|
||||
@@ -159,7 +159,7 @@ function CreateAttempt({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center w-4/5">
|
||||
<div className="flex items-center">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Each time you start an attempt, a new session is initiated with your
|
||||
selected coding agent, and a git worktree and corresponding task
|
||||
@@ -167,7 +167,7 @@ function CreateAttempt({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 items-end">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 items-end">
|
||||
{/* Step 1: Choose Base Branch */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -183,136 +183,147 @@ function CreateAttempt({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Step 2: Choose Profile and Mode */}
|
||||
{/* Step 2: Choose Profile */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Profile
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{availableProfiles && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 justify-between text-xs"
|
||||
{availableProfiles && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full justify-between text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Settings2 className="h-3 w-3" />
|
||||
<span className="truncate">
|
||||
{selectedProfile?.profile || 'Select profile'}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{availableProfiles.map((profile) => (
|
||||
<DropdownMenuItem
|
||||
key={profile.label}
|
||||
onClick={() => {
|
||||
setSelectedProfile({
|
||||
profile: profile.label,
|
||||
variant: null,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.profile === profile.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Settings2 className="h-3 w-3" />
|
||||
<span className="truncate">
|
||||
{selectedProfile?.profile || 'Select profile'}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{availableProfiles.map((profile) => (
|
||||
<DropdownMenuItem
|
||||
key={profile.label}
|
||||
onClick={() => {
|
||||
setSelectedProfile({
|
||||
profile: profile.label,
|
||||
variant: null,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.profile === profile.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
{profile.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 3: Choose Variant (if available) */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">
|
||||
Variant
|
||||
</label>
|
||||
</div>
|
||||
{(() => {
|
||||
const currentProfile = availableProfiles?.find(
|
||||
(p) => p.label === selectedProfile?.profile
|
||||
);
|
||||
const hasVariants =
|
||||
currentProfile?.variants && currentProfile.variants.length > 0;
|
||||
|
||||
if (hasVariants && currentProfile) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full px-2 flex items-center justify-between text-xs"
|
||||
>
|
||||
{profile.label}
|
||||
<span className="truncate flex-1 text-left">
|
||||
{selectedProfile?.variant || 'Default'}
|
||||
</span>
|
||||
<ArrowDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={!selectedProfile?.variant ? 'bg-accent' : ''}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{/* Show variant dropdown or disabled button */}
|
||||
{(() => {
|
||||
const currentProfile = availableProfiles?.find(
|
||||
(p) => p.label === selectedProfile?.profile
|
||||
);
|
||||
const hasVariants =
|
||||
currentProfile?.variants &&
|
||||
currentProfile.variants.length > 0;
|
||||
|
||||
if (hasVariants) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-24 px-2 flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="truncate flex-1 text-left">
|
||||
{selectedProfile?.variant || 'Default'}
|
||||
</span>
|
||||
<ArrowDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{currentProfile.variants.map((variant) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: null,
|
||||
variant: variant.label,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={
|
||||
!selectedProfile?.variant ? 'bg-accent' : ''
|
||||
selectedProfile?.variant === variant.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
Default
|
||||
{variant.label}
|
||||
</DropdownMenuItem>
|
||||
{currentProfile.variants.map((variant) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: variant.label,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.variant === variant.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variant.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
} else if (currentProfile) {
|
||||
// Show disabled button when profile exists but has no variants
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-24 px-2 flex items-center justify-between text-xs"
|
||||
disabled
|
||||
>
|
||||
<span className="truncate flex-1 text-left">Default</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
if (currentProfile) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start"
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-full text-xs justify-start"
|
||||
>
|
||||
Select profile first
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Step 3: Start Attempt */}
|
||||
{/* Step 4: Start Attempt */}
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
onClick={handleCreateAttempt}
|
||||
@@ -320,7 +331,9 @@ function CreateAttempt({
|
||||
!selectedProfile || !createAttemptBranch || isAttemptRunning
|
||||
}
|
||||
size="sm"
|
||||
className={'w-full text-xs gap-2'}
|
||||
className={
|
||||
'w-full text-xs gap-2 justify-center bg-black text-white hover:bg-black/90'
|
||||
}
|
||||
title={
|
||||
!createAttemptBranch
|
||||
? 'Base branch is required'
|
||||
@@ -355,7 +368,12 @@ function CreateAttempt({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleConfirmCreateAttempt}>Start</Button>
|
||||
<Button
|
||||
onClick={handleConfirmCreateAttempt}
|
||||
className="bg-black text-white hover:bg-black/90"
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -124,6 +124,7 @@ function CreatePrDialog({
|
||||
prBaseBranch,
|
||||
prBody,
|
||||
prTitle,
|
||||
fetchAttemptData,
|
||||
setCreatingPR,
|
||||
setError,
|
||||
setShowCreatePRDialog,
|
||||
|
||||
@@ -38,11 +38,9 @@ import {
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { ExecutionProcess } from 'shared/types';
|
||||
import type { GitBranch, TaskAttempt } from 'shared/types';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
@@ -111,9 +109,6 @@ function CurrentAttempt({
|
||||
const [merging, setMerging] = useState(false);
|
||||
const [pushing, setPushing] = useState(false);
|
||||
const [rebasing, setRebasing] = useState(false);
|
||||
const [devServerDetails, setDevServerDetails] =
|
||||
useState<ExecutionProcess | null>(null);
|
||||
const [isHoveringDevServer, setIsHoveringDevServer] = useState(false);
|
||||
const [showRebaseDialog, setShowRebaseDialog] = useState(false);
|
||||
const [selectedRebaseBranch, setSelectedRebaseBranch] = useState<string>('');
|
||||
const [showStopConfirmation, setShowStopConfirmation] = useState(false);
|
||||
@@ -121,14 +116,6 @@ function CurrentAttempt({
|
||||
const [mergeSuccess, setMergeSuccess] = useState(false);
|
||||
const [pushSuccess, setPushSuccess] = useState(false);
|
||||
|
||||
const processedDevServerLogs = useMemo(() => {
|
||||
if (!devServerDetails) return 'No output yet...';
|
||||
|
||||
// TODO: stdout/stderr fields need to be restored to ExecutionProcess type
|
||||
// For now, show basic status information
|
||||
return `Status: ${devServerDetails.status}\nStarted: ${devServerDetails.started_at}`;
|
||||
}, [devServerDetails]);
|
||||
|
||||
// Find running dev server in current project
|
||||
const runningDevServer = useMemo(() => {
|
||||
return attemptData.processes.find(
|
||||
@@ -147,30 +134,6 @@ function CurrentAttempt({
|
||||
)[0];
|
||||
}, [attemptData.processes]);
|
||||
|
||||
const fetchDevServerDetails = useCallback(async () => {
|
||||
if (!runningDevServer || !task || !selectedAttempt) return;
|
||||
|
||||
try {
|
||||
const result = await executionProcessesApi.getDetails(
|
||||
runningDevServer.id
|
||||
);
|
||||
setDevServerDetails(result);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch dev server details:', err);
|
||||
}
|
||||
}, [runningDevServer, task, selectedAttempt, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHoveringDevServer || !runningDevServer) {
|
||||
setDevServerDetails(null);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchDevServerDetails();
|
||||
const interval = setInterval(fetchDevServerDetails, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isHoveringDevServer, runningDevServer, fetchDevServerDetails]);
|
||||
|
||||
const startDevServer = async () => {
|
||||
if (!task || !selectedAttempt) return;
|
||||
|
||||
@@ -479,8 +442,9 @@ function CurrentAttempt({
|
||||
}, [mergeInfo, branchStatus]);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-6 items-start">
|
||||
<div className="space-y-2 @container">
|
||||
{/* <div className="flex gap-6 items-start"> */}
|
||||
<div className="grid grid-cols-2 gap-3 items-start @md:flex @md:items-start">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">
|
||||
Profile
|
||||
@@ -538,7 +502,7 @@ function CurrentAttempt({
|
||||
{(() => {
|
||||
const statusInfo = getStatusInfo();
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<>
|
||||
<div
|
||||
className={`h-2 w-2 ${statusInfo.dotColor} rounded-full`}
|
||||
/>
|
||||
@@ -551,19 +515,19 @@ function CurrentAttempt({
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className={`text-sm font-medium ${statusInfo.textColor}`}
|
||||
className={`text-sm font-medium ${statusInfo.textColor} truncate`}
|
||||
>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1 pt-1">
|
||||
Path
|
||||
@@ -601,89 +565,50 @@ function CurrentAttempt({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={!projectHasDevScript ? 'cursor-not-allowed' : ''}
|
||||
onMouseEnter={() => setIsHoveringDevServer(true)}
|
||||
onMouseLeave={() => setIsHoveringDevServer(false)}
|
||||
>
|
||||
<Button
|
||||
variant={runningDevServer ? 'destructive' : 'outline'}
|
||||
size="xs"
|
||||
onClick={runningDevServer ? stopDevServer : startDevServer}
|
||||
disabled={isStartingDevServer || !projectHasDevScript}
|
||||
className="gap-1"
|
||||
>
|
||||
{runningDevServer ? (
|
||||
<>
|
||||
<StopCircle className="h-3 w-3" />
|
||||
Stop Dev
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-3 w-3" />
|
||||
Dev
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={runningDevServer ? 'max-w-2xl p-4' : ''}
|
||||
side="top"
|
||||
align="center"
|
||||
avoidCollisions={true}
|
||||
>
|
||||
{!projectHasDevScript ? (
|
||||
<p>
|
||||
Add a dev server script in project settings to enable this
|
||||
feature
|
||||
</p>
|
||||
) : runningDevServer && devServerDetails ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">
|
||||
Dev Server Logs (Last 10 lines):
|
||||
</p>
|
||||
<pre className="text-xs bg-muted p-2 rounded max-h-64 overflow-y-auto whitespace-pre-wrap">
|
||||
{processedDevServerLogs}
|
||||
</pre>
|
||||
</div>
|
||||
) : runningDevServer ? (
|
||||
<p>Stop the running dev server</p>
|
||||
) : (
|
||||
<p>Start the dev server</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-3 @md:flex @md:flex-wrap @md:items-center">
|
||||
<div className="flex gap-2 @md:flex-none">
|
||||
<Button
|
||||
variant={runningDevServer ? 'destructive' : 'outline'}
|
||||
size="xs"
|
||||
onClick={runningDevServer ? stopDevServer : startDevServer}
|
||||
disabled={isStartingDevServer || !projectHasDevScript}
|
||||
className="gap-1 flex-1"
|
||||
>
|
||||
{runningDevServer ? (
|
||||
<>
|
||||
<StopCircle className="h-3 w-3" />
|
||||
Stop Dev
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-3 w-3" />
|
||||
Dev
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* View Dev Server Logs Button */}
|
||||
{latestDevServerProcess && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleViewDevServerLogs}
|
||||
className="gap-1"
|
||||
>
|
||||
<ScrollText className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View dev server logs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* View Dev Server Logs Button */}
|
||||
{latestDevServerProcess && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleViewDevServerLogs}
|
||||
className="gap-1"
|
||||
>
|
||||
<ScrollText className="h-3 w-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View dev server logs</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
{/* Git Operations */}
|
||||
{selectedAttempt && branchStatus && !mergeInfo.hasMergedPR && (
|
||||
<>
|
||||
@@ -755,67 +680,69 @@ function CurrentAttempt({
|
||||
</>
|
||||
)}
|
||||
|
||||
{isStopping || isAttemptRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="xs"
|
||||
onClick={stopAllExecutions}
|
||||
disabled={isStopping}
|
||||
className="gap-2"
|
||||
>
|
||||
<StopCircle className="h-4 w-4" />
|
||||
{isStopping ? 'Stopping...' : 'Stop Attempt'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleEnterCreateAttemptMode}
|
||||
className="gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Attempt
|
||||
</Button>
|
||||
)}
|
||||
{taskAttempts.length > 1 && (
|
||||
<DropdownMenu>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="xs" className="gap-2">
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View attempt history</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
{taskAttempts.map((attempt) => (
|
||||
<DropdownMenuItem
|
||||
key={attempt.id}
|
||||
onClick={() => handleAttemptChange(attempt)}
|
||||
className={
|
||||
selectedAttempt?.id === attempt.id ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="font-medium text-sm">
|
||||
{new Date(attempt.created_at).toLocaleDateString()}{' '}
|
||||
{new Date(attempt.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{attempt.profile || 'Base Agent'}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<div className="flex gap-2 @md:flex-none">
|
||||
{isStopping || isAttemptRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="xs"
|
||||
onClick={stopAllExecutions}
|
||||
disabled={isStopping}
|
||||
className="gap-1 flex-1"
|
||||
>
|
||||
<StopCircle className="h-4 w-4" />
|
||||
{isStopping ? 'Stopping...' : 'Stop Attempt'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleEnterCreateAttemptMode}
|
||||
className="gap-1 flex-1"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
New Attempt
|
||||
</Button>
|
||||
)}
|
||||
{taskAttempts.length > 1 && (
|
||||
<DropdownMenu>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="xs" className="gap-1">
|
||||
<History className="h-3 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>View attempt history</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent align="start" className="w-64">
|
||||
{taskAttempts.map((attempt) => (
|
||||
<DropdownMenuItem
|
||||
key={attempt.id}
|
||||
onClick={() => handleAttemptChange(attempt)}
|
||||
className={
|
||||
selectedAttempt?.id === attempt.id ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full">
|
||||
<span className="font-medium text-sm">
|
||||
{new Date(attempt.created_at).toLocaleDateString()}{' '}
|
||||
{new Date(attempt.created_at).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{attempt.profile || 'Base Agent'}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,52 +19,69 @@ export const PANEL_WIDTHS = {
|
||||
} as const;
|
||||
|
||||
// Generate classes for TaskDetailsPanel
|
||||
export const getTaskPanelClasses = () => {
|
||||
const overlayClasses = [
|
||||
'fixed inset-y-0 right-0 z-50',
|
||||
PANEL_WIDTHS.base,
|
||||
PANEL_WIDTHS.sm,
|
||||
PANEL_WIDTHS.md,
|
||||
PANEL_WIDTHS.lg,
|
||||
PANEL_WIDTHS.xl,
|
||||
].join(' ');
|
||||
export const getTaskPanelClasses = (forceFullScreen: boolean) => {
|
||||
const overlayClasses = forceFullScreen
|
||||
? 'fixed inset-y-0 right-0 z-50 w-full'
|
||||
: [
|
||||
'fixed inset-y-0 right-0 z-50',
|
||||
PANEL_WIDTHS.base,
|
||||
PANEL_WIDTHS.sm,
|
||||
PANEL_WIDTHS.md,
|
||||
PANEL_WIDTHS.lg,
|
||||
PANEL_WIDTHS.xl,
|
||||
].join(' ');
|
||||
|
||||
const sideBySideClasses = [
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:relative`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:inset-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:z-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:w-[800px]`,
|
||||
].join(' ');
|
||||
const sideBySideClasses = forceFullScreen
|
||||
? ''
|
||||
: [
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:relative`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:inset-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:z-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:w-[800px]`,
|
||||
].join(' ');
|
||||
|
||||
return `${overlayClasses} ${sideBySideClasses} bg-background border-l shadow-lg overflow-hidden`;
|
||||
};
|
||||
|
||||
// Generate classes for backdrop (only show in overlay mode)
|
||||
export const getBackdropClasses = () => {
|
||||
return `fixed inset-0 z-40 bg-background/80 backdrop-blur-sm ${PANEL_SIDE_BY_SIDE_BREAKPOINT}:hidden`;
|
||||
export const getBackdropClasses = (forceFullScreen: boolean) => {
|
||||
return `fixed inset-0 z-40 bg-background/80 backdrop-blur-sm ${PANEL_SIDE_BY_SIDE_BREAKPOINT}:hidden ${forceFullScreen ? '' : 'hidden'}`;
|
||||
};
|
||||
|
||||
// Generate classes for main container (enable flex layout in side-by-side mode)
|
||||
export const getMainContainerClasses = (isPanelOpen: boolean) => {
|
||||
if (!isPanelOpen) return 'w-full';
|
||||
export const getMainContainerClasses = (
|
||||
isPanelOpen: boolean,
|
||||
forceFullScreen: boolean
|
||||
) => {
|
||||
const overlayClasses =
|
||||
isPanelOpen && forceFullScreen
|
||||
? 'w-full'
|
||||
: `${PANEL_SIDE_BY_SIDE_BREAKPOINT}:flex ${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`;
|
||||
|
||||
return `w-full ${PANEL_SIDE_BY_SIDE_BREAKPOINT}:flex ${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`;
|
||||
return `${overlayClasses}`;
|
||||
};
|
||||
|
||||
// Generate classes for kanban section
|
||||
export const getKanbanSectionClasses = (isPanelOpen: boolean) => {
|
||||
export const getKanbanSectionClasses = (
|
||||
isPanelOpen: boolean,
|
||||
forceFullScreen: boolean
|
||||
) => {
|
||||
if (!isPanelOpen) return 'w-full';
|
||||
|
||||
const overlayClasses = 'w-full opacity-50 pointer-events-none';
|
||||
const sideBySideClasses = [
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:flex-1`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:min-w-0`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:overflow-y-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:opacity-100`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:pointer-events-auto`,
|
||||
].join(' ');
|
||||
// const overlayClasses = 'w-full opacity-50 pointer-events-none';
|
||||
const sideBySideClasses =
|
||||
isPanelOpen && forceFullScreen
|
||||
? ''
|
||||
: [
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:flex-1`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:min-w-0`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:h-full`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:overflow-y-auto`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:opacity-100`,
|
||||
`${PANEL_SIDE_BY_SIDE_BREAKPOINT}:pointer-events-auto`,
|
||||
].join(' ');
|
||||
|
||||
return `${overlayClasses} ${sideBySideClasses}`;
|
||||
// return `${overlayClasses} ${sideBySideClasses}`;
|
||||
return `${sideBySideClasses}`;
|
||||
};
|
||||
|
||||
@@ -83,6 +83,9 @@ export function ProjectTasks() {
|
||||
setIsTaskDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
// Full screen
|
||||
const [fullScreenTaskDetails, setFullScreenTaskDetails] = useState(false);
|
||||
|
||||
const handleOpenInIDE = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
@@ -380,9 +383,13 @@ export function ProjectTasks() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getMainContainerClasses(isPanelOpen)}>
|
||||
<div
|
||||
className={getMainContainerClasses(isPanelOpen, fullScreenTaskDetails)}
|
||||
>
|
||||
{/* Left Column - Kanban Section */}
|
||||
<div className={getKanbanSectionClasses(isPanelOpen)}>
|
||||
<div
|
||||
className={getKanbanSectionClasses(isPanelOpen, fullScreenTaskDetails)}
|
||||
>
|
||||
{/* Header */}
|
||||
|
||||
<div className="px-8 my-12 flex flex-row">
|
||||
@@ -527,6 +534,8 @@ export function ProjectTasks() {
|
||||
onEditTask={handleEditTask}
|
||||
onDeleteTask={handleDeleteTask}
|
||||
isDialogOpen={isTaskDialogOpen || isProjectSettingsOpen}
|
||||
isFullScreen={fullScreenTaskDetails}
|
||||
setFullScreen={setFullScreenTaskDetails}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -121,5 +121,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/container-queries")],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user