import { useState, useEffect, useCallback } from "react"; import { useTaskSubscription } from "../../hooks/useTaskSubscription"; import type { TaskOutputEvent } from "../../hooks/useTaskSubscription"; import { TaskOutput } from "../mesh/TaskOutput"; import { WorktreeFilesPanel } from "../mesh/WorktreeFilesPanel"; import { OverlayDiffViewer } from "../mesh/OverlayDiffViewer"; import { getTaskOutput, getTaskDiff } from "../../lib/api"; interface TaskSlideOutPanelProps { taskId: string; taskName?: string; isOpen: boolean; onClose: () => void; } export function TaskSlideOutPanel({ taskId, taskName, isOpen, onClose, }: TaskSlideOutPanelProps) { const [entries, setEntries] = useState([]); const [isStreaming, setIsStreaming] = useState(false); const [loadingHistory, setLoadingHistory] = useState(false); const [showDiff, setShowDiff] = useState(false); const [diffContent, setDiffContent] = useState(""); const [diffLoading, setDiffLoading] = useState(false); const [selectedFileDiff, setSelectedFileDiff] = useState(null); const [selectedFilePath, setSelectedFilePath] = useState(null); // Escape key handler useEffect(() => { const handleEsc = (e: KeyboardEvent) => { // If diff is showing, close diff first; otherwise close panel if (e.key === "Escape") { if (selectedFileDiff !== null || diffLoading) { setSelectedFileDiff(null); setSelectedFilePath(null); setDiffLoading(false); } else { onClose(); } } }; if (isOpen) document.addEventListener("keydown", handleEsc); return () => document.removeEventListener("keydown", handleEsc); }, [isOpen, onClose, selectedFileDiff, diffLoading]); // Load historical output when panel opens with a taskId useEffect(() => { if (!isOpen || !taskId) { setEntries([]); setIsStreaming(false); // Reset diff state when panel closes setSelectedFileDiff(null); setSelectedFilePath(null); setDiffLoading(false); return; } let cancelled = false; setLoadingHistory(true); getTaskOutput(taskId) .then((res) => { if (cancelled) return; // Map TaskOutputEntry to TaskOutputEvent const mapped: TaskOutputEvent[] = res.entries.map((e) => ({ taskId: e.taskId, messageType: e.messageType, content: e.content, toolName: e.toolName, toolInput: e.toolInput, isError: e.isError, costUsd: e.costUsd, durationMs: e.durationMs, isPartial: false, })); setEntries(mapped); }) .catch((err) => { if (cancelled) return; console.error("Failed to load task output history:", err); }) .finally(() => { if (!cancelled) setLoadingHistory(false); }); return () => { cancelled = true; }; }, [isOpen, taskId]); // Handle live output events const handleOutput = useCallback( (event: TaskOutputEvent) => { if (event.isPartial) return; setEntries((prev) => [...prev, event]); setIsStreaming(true); }, [] ); // Handle task updates (to detect completion) const handleUpdate = useCallback( (event: { status: string }) => { if ( event.status === "completed" || event.status === "failed" || event.status === "cancelled" ) { setIsStreaming(false); } else if (event.status === "running") { setIsStreaming(true); } }, [] ); // Handle file click to show diff const handleFileClick = useCallback(async (_filePath: string) => { if (!taskId) return; setDiffLoading(true); try { const result = await getTaskDiff(taskId); if (result.success && result.diff) { setDiffContent(result.diff); setShowDiff(true); } } catch (e) { console.error("Failed to get diff:", e); } finally { setDiffLoading(false); } }, [taskId]); // Subscribe to live output useTaskSubscription({ taskId: isOpen ? taskId : null, subscribeOutput: isOpen && !!taskId, onOutput: handleOutput, onUpdate: handleUpdate, }); const showingDiff = selectedFileDiff !== null || diffLoading; return ( <> {/* Backdrop overlay */}
{/* Slide-out panel */}
{/* Header */}
{showingDiff && ( )} {showingDiff ? "Diff" : "Task"} {showingDiff ? (selectedFilePath || "Loading...") : (taskName || taskId)} {!showingDiff && isStreaming && ( Live )}
{/* Content */}
{showingDiff ? ( /* Diff view replaces the worktree panel content */
{ setSelectedFileDiff(null); setSelectedFilePath(null); setDiffLoading(false); }} title={selectedFilePath ? `Diff: ${selectedFilePath}` : "File Diff"} />
) : ( <> {/* Task Output section (~60% height) */}
{loadingHistory ? (
Loading output...
) : ( )}
{/* Worktree Changes section (~40% height) */}
{taskId && }
)}
{/* Diff modal */} {showDiff && (
setShowDiff(false)} title={`Changes in ${taskName || taskId}`} />
)} ); }