diff options
Diffstat (limited to 'makima/frontend/src/components/directives/TaskSlideOutPanel.tsx')
| -rw-r--r-- | makima/frontend/src/components/directives/TaskSlideOutPanel.tsx | 145 |
1 files changed, 122 insertions, 23 deletions
diff --git a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx index 29fce23..176728c 100644 --- a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx +++ b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx @@ -3,7 +3,8 @@ import { useTaskSubscription } from "../../hooks/useTaskSubscription"; import type { TaskOutputEvent } from "../../hooks/useTaskSubscription"; import { TaskOutput } from "../mesh/TaskOutput"; import { WorktreeFilesPanel } from "../mesh/WorktreeFilesPanel"; -import { getTaskOutput } from "../../lib/api"; +import { OverlayDiffViewer } from "../mesh/OverlayDiffViewer"; +import { getTaskOutput, getTaskDiff } from "../../lib/api"; interface TaskSlideOutPanelProps { taskId: string; @@ -21,21 +22,37 @@ export function TaskSlideOutPanel({ const [entries, setEntries] = useState<TaskOutputEvent[]>([]); const [isStreaming, setIsStreaming] = useState(false); const [loadingHistory, setLoadingHistory] = useState(false); + const [showDiff, setShowDiff] = useState(false); + const [diffContent, setDiffContent] = useState<string>(""); + const [diffLoading, setDiffLoading] = useState(false); // Escape key handler useEffect(() => { const handleEsc = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); + // 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]); + }, [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; } @@ -98,6 +115,23 @@ export function TaskSlideOutPanel({ [] ); + // 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, @@ -106,6 +140,8 @@ export function TaskSlideOutPanel({ onUpdate: handleUpdate, }); + const showingDiff = selectedFileDiff !== null || diffLoading; + return ( <> {/* Backdrop overlay */} @@ -125,13 +161,27 @@ export function TaskSlideOutPanel({ {/* Header */} <div className="flex items-center justify-between px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)] shrink-0"> <div className="flex items-center gap-2 min-w-0 flex-1"> + {showingDiff && ( + <button + type="button" + onClick={() => { + setSelectedFileDiff(null); + setSelectedFilePath(null); + setDiffLoading(false); + }} + className="text-[#75aafc] hover:text-white font-mono text-xs transition-colors shrink-0 mr-1" + title="Back to worktree view" + > + ← + </button> + )} <span className="text-[10px] font-mono text-[#75aafc] uppercase tracking-wide shrink-0"> - Task + {showingDiff ? "Diff" : "Task"} </span> <span className="text-[12px] font-mono text-white truncate"> - {taskName || taskId} + {showingDiff ? (selectedFilePath || "Loading...") : (taskName || taskId)} </span> - {isStreaming && ( + {!showingDiff && isStreaming && ( <span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-400/10 border border-green-400/30 shrink-0"> <span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" /> <span className="text-green-400 font-mono text-[9px] uppercase"> @@ -141,39 +191,88 @@ export function TaskSlideOutPanel({ )} </div> <button + onClick={async () => { + 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); + } + }} + className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors px-1.5 py-0.5 border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] shrink-0" + > + {diffLoading ? "Loading..." : "View Diff"} + </button> + <button type="button" onClick={onClose} className="text-[#7788aa] hover:text-white font-mono text-sm transition-colors ml-2 shrink-0 w-6 h-6 flex items-center justify-center" > - ✕ + ✕ </button> </div> {/* Content */} <div className="flex-1 flex flex-col min-h-0 overflow-hidden"> - {/* Task Output section (~60% height) */} - <div className="flex-[3] min-h-0 flex flex-col border-b border-[rgba(117,170,252,0.15)]"> - {loadingHistory ? ( - <div className="flex-1 flex items-center justify-center"> - <span className="font-mono text-xs text-[#555] animate-pulse"> - Loading output... - </span> - </div> - ) : ( - <TaskOutput - entries={entries} - isStreaming={isStreaming} - taskId={taskId} + {showingDiff ? ( + /* Diff view replaces the worktree panel content */ + <div className="flex-1 min-h-0 overflow-y-auto"> + <OverlayDiffViewer + diff={selectedFileDiff || ""} + loading={diffLoading} + onClose={() => { + setSelectedFileDiff(null); + setSelectedFilePath(null); + setDiffLoading(false); + }} + title={selectedFilePath ? `Diff: ${selectedFilePath}` : "File Diff"} /> - )} - </div> + </div> + ) : ( + <> + {/* Task Output section (~60% height) */} + <div className="flex-[3] min-h-0 flex flex-col border-b border-[rgba(117,170,252,0.15)]"> + {loadingHistory ? ( + <div className="flex-1 flex items-center justify-center"> + <span className="font-mono text-xs text-[#555] animate-pulse"> + Loading output... + </span> + </div> + ) : ( + <TaskOutput + entries={entries} + isStreaming={isStreaming} + taskId={taskId} + /> + )} + </div> {/* Worktree Changes section (~40% height) */} <div className="flex-[2] min-h-0 overflow-y-auto"> - {taskId && <WorktreeFilesPanel taskId={taskId} />} + {taskId && <WorktreeFilesPanel taskId={taskId} onFileClick={handleFileClick} />} </div> </div> </div> + + {/* Diff modal */} + {showDiff && ( + <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"> + <div className="max-w-4xl w-full max-h-[80vh]"> + <OverlayDiffViewer + diff={diffContent} + onClose={() => setShowDiff(false)} + title={`Changes in ${taskName || taskId}`} + /> + </div> + </div> + )} </> ); } |
