Files
vibe-kanban/frontend/src/components/tasks/TaskDetailsPanel.tsx
Gabriel Gordon-Hall 340b094c75 chore: setup CI scripts (#6)
* wip: workflows

* wip: fix up issues in ci scripts and fix frontend lint errors

* wip: fix backend lints

* remove unused deps

* wip: build frontend in test.yml

* wip: attempt to improve Rust caching

* wip: testing release

* wip: linear release flow

* wip: check against both package.json versions

* wip: spurious attempt to get Rust caching

* wip: more cache

* merge release and publish jobs; add more caching to release flow

* decouple github releases and npm publishing

* update pack flow

---------

Co-authored-by: couscous <couscous@runner.com>
2025-06-27 13:32:32 +01:00

211 lines
6.7 KiB
TypeScript

import { useEffect, useRef, useCallback, useState } from 'react';
import { TaskDetailsHeader } from './TaskDetailsHeader';
import { TaskDetailsToolbar } from './TaskDetailsToolbar';
import { TaskActivityHistory } from './TaskActivityHistory';
import { TaskFollowUpSection } from './TaskFollowUpSection';
import { EditorSelectionDialog } from './EditorSelectionDialog';
import { useTaskDetails } from '@/hooks/useTaskDetails';
import {
getTaskPanelClasses,
getBackdropClasses,
} from '@/lib/responsive-config';
import type { TaskWithAttemptStatus, EditorType, Project } from 'shared/types';
interface TaskDetailsPanelProps {
task: TaskWithAttemptStatus | null;
project: Project | null;
projectId: string;
isOpen: boolean;
onClose: () => void;
onEditTask?: (task: TaskWithAttemptStatus) => void;
onDeleteTask?: (taskId: string) => void;
isDialogOpen?: boolean; // New prop to indicate if any dialog is open
}
export function TaskDetailsPanel({
task,
project,
projectId,
isOpen,
onClose,
onEditTask,
onDeleteTask,
isDialogOpen = false,
}: TaskDetailsPanelProps) {
const [showEditorDialog, setShowEditorDialog] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Use the custom hook for all task details logic
const {
taskAttempts,
selectedAttempt,
attemptData,
loading,
selectedExecutor,
isStopping,
followUpMessage,
isSendingFollowUp,
followUpError,
isStartingDevServer,
devServerDetails,
runningDevServer,
isAttemptRunning,
canSendFollowUp,
processedDevServerLogs,
setSelectedExecutor,
setFollowUpMessage,
setFollowUpError,
setIsHoveringDevServer,
handleAttemptChange,
createNewAttempt,
stopAllExecutions,
startDevServer,
stopDevServer,
openInEditor,
handleSendFollowUp,
} = useTaskDetails(task, projectId, isOpen);
// Handle ESC key locally to prevent global navigation
useEffect(() => {
if (!isOpen || isDialogOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
onClose();
}
};
document.addEventListener('keydown', handleKeyDown, true);
return () => document.removeEventListener('keydown', handleKeyDown, true);
}, [isOpen, onClose, isDialogOpen]);
// Auto-scroll to bottom when activities or execution processes change
useEffect(() => {
if (shouldAutoScroll && scrollContainerRef.current) {
scrollContainerRef.current.scrollTop =
scrollContainerRef.current.scrollHeight;
}
}, [attemptData.activities, attemptData.processes, shouldAutoScroll]);
// Handle scroll events to detect manual scrolling
const handleScroll = useCallback(() => {
if (scrollContainerRef.current) {
const { scrollTop, scrollHeight, clientHeight } =
scrollContainerRef.current;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5;
if (isAtBottom && !shouldAutoScroll) {
setShouldAutoScroll(true);
} else if (!isAtBottom && shouldAutoScroll) {
setShouldAutoScroll(false);
}
}
}, [shouldAutoScroll]);
const handleOpenInEditor = async (editorType?: EditorType) => {
try {
await openInEditor(editorType);
} catch (err) {
if (!editorType) {
setShowEditorDialog(true);
}
}
};
if (!task) return null;
return (
<>
{isOpen && (
<>
{/* Backdrop - only on smaller screens (overlay mode) */}
<div className={getBackdropClasses()} onClick={onClose} />
{/* Panel */}
<div className={getTaskPanelClasses()}>
<div className="flex flex-col h-full">
{/* Header */}
<TaskDetailsHeader
task={task}
onClose={onClose}
onEditTask={onEditTask}
onDeleteTask={onDeleteTask}
/>
{/* Toolbar */}
<TaskDetailsToolbar
task={task}
project={project}
projectId={projectId}
selectedAttempt={selectedAttempt}
taskAttempts={taskAttempts}
isAttemptRunning={isAttemptRunning}
isStopping={isStopping}
selectedExecutor={selectedExecutor}
runningDevServer={runningDevServer}
isStartingDevServer={isStartingDevServer}
devServerDetails={devServerDetails}
processedDevServerLogs={processedDevServerLogs}
onAttemptChange={handleAttemptChange}
onCreateNewAttempt={createNewAttempt}
onStopAllExecutions={stopAllExecutions}
onSetSelectedExecutor={setSelectedExecutor}
onStartDevServer={startDevServer}
onStopDevServer={stopDevServer}
onOpenInEditor={handleOpenInEditor}
onSetIsHoveringDevServer={setIsHoveringDevServer}
/>
{/* Content */}
<div
ref={scrollContainerRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto p-6 space-y-6"
>
{loading ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-foreground mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
) : (
<TaskActivityHistory
selectedAttempt={selectedAttempt}
activities={attemptData.activities}
runningProcessDetails={attemptData.runningProcessDetails}
/>
)}
</div>
{/* Footer - Follow-up section */}
{selectedAttempt && (
<TaskFollowUpSection
followUpMessage={followUpMessage}
setFollowUpMessage={setFollowUpMessage}
isSendingFollowUp={isSendingFollowUp}
followUpError={followUpError}
setFollowUpError={setFollowUpError}
canSendFollowUp={canSendFollowUp}
isAttemptRunning={isAttemptRunning}
projectId={projectId}
onSendFollowUp={handleSendFollowUp}
/>
)}
</div>
</div>
{/* Editor Selection Dialog */}
<EditorSelectionDialog
isOpen={showEditorDialog}
onClose={() => setShowEditorDialog(false)}
onSelectEditor={handleOpenInEditor}
/>
</>
)}
</>
);
}