diff options
Diffstat (limited to 'makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx')
| -rw-r--r-- | makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx new file mode 100644 index 0000000..b529588 --- /dev/null +++ b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx @@ -0,0 +1,197 @@ +import { useState, useEffect, useCallback } from "react"; +import type { WorktreeInfo } from "../../lib/api"; +import { getWorktreeInfo } from "../../lib/api"; + +interface WorktreeFilesPanelProps { + taskId: string; +} + +/** Get status badge styling based on file status */ +function getStatusStyle(status: string): { color: string; bgColor: string; label: string } { + switch (status) { + case "M": + case "modified": + return { color: "text-yellow-400", bgColor: "bg-yellow-400/10", label: "M" }; + case "A": + case "added": + return { color: "text-green-400", bgColor: "bg-green-400/10", label: "A" }; + case "D": + case "deleted": + return { color: "text-red-400", bgColor: "bg-red-400/10", label: "D" }; + case "R": + case "renamed": + return { color: "text-cyan-400", bgColor: "bg-cyan-400/10", label: "R" }; + case "C": + case "copied": + return { color: "text-purple-400", bgColor: "bg-purple-400/10", label: "C" }; + case "U": + case "unmerged": + return { color: "text-orange-400", bgColor: "bg-orange-400/10", label: "U" }; + case "?": + case "untracked": + return { color: "text-[#555]", bgColor: "bg-[#555]/10", label: "?" }; + default: + return { color: "text-[#9bc3ff]", bgColor: "bg-[rgba(117,170,252,0.1)]", label: status.charAt(0).toUpperCase() }; + } +} + +export function WorktreeFilesPanel({ taskId }: WorktreeFilesPanelProps) { + const [worktreeInfo, setWorktreeInfo] = useState<WorktreeInfo | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + const [expanded, setExpanded] = useState(false); + + const fetchWorktreeInfo = useCallback(async () => { + try { + setLoading(true); + setError(null); + const info = await getWorktreeInfo(taskId); + setWorktreeInfo(info); + } catch (e) { + console.error("Failed to fetch worktree info:", e); + setError(e instanceof Error ? e.message : "Failed to fetch worktree info"); + } finally { + setLoading(false); + } + }, [taskId]); + + useEffect(() => { + fetchWorktreeInfo(); + }, [fetchWorktreeInfo]); + + if (loading) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase mb-2"> + Worktree Changes + </div> + <div className="font-mono text-xs text-[#555] animate-pulse"> + Loading worktree info... + </div> + </div> + ); + } + + if (error) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="flex items-center justify-between mb-2"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Worktree Changes + </div> + <button + onClick={fetchWorktreeInfo} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Retry + </button> + </div> + <div className="font-mono text-xs text-red-400"> + {error} + </div> + </div> + ); + } + + if (!worktreeInfo || worktreeInfo.files.length === 0) { + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3"> + <div className="flex items-center justify-between mb-2"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Worktree Changes + </div> + <button + onClick={fetchWorktreeInfo} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + Refresh + </button> + </div> + <div className="font-mono text-xs text-[#555]"> + No changes in worktree + </div> + </div> + ); + } + + const { stats, files } = worktreeInfo; + const displayFiles = expanded ? files : files.slice(0, 10); + + return ( + <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)]"> + {/* Header */} + <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.1)]"> + <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase"> + Worktree Changes + </div> + <div className="flex items-center gap-3"> + {/* Stats */} + <div className="flex items-center gap-2 font-mono text-[10px]"> + <span className="text-[#75aafc]"> + {stats.filesChanged} file{stats.filesChanged !== 1 ? "s" : ""} + </span> + <span className="text-green-400">+{stats.insertions}</span> + <span className="text-red-400">-{stats.deletions}</span> + </div> + <button + onClick={fetchWorktreeInfo} + className="font-mono text-[10px] text-[#555] hover:text-[#75aafc] transition-colors" + title="Refresh" + > + <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> + </svg> + </button> + </div> + </div> + + {/* File list */} + <div className="divide-y divide-[rgba(117,170,252,0.05)]"> + {displayFiles.map((file) => { + const statusStyle = getStatusStyle(file.status); + return ( + <div + key={file.path} + className="flex items-center gap-2 px-3 py-1.5 hover:bg-[rgba(117,170,252,0.03)]" + > + {/* Status badge */} + <span + className={`w-5 h-5 flex items-center justify-center font-mono text-[10px] font-medium ${statusStyle.color} ${statusStyle.bgColor} border border-current/20`} + title={file.status} + > + {statusStyle.label} + </span> + + {/* File path */} + <span className="flex-1 font-mono text-xs text-[#dbe7ff] truncate" title={file.path}> + {file.path} + </span> + + {/* Line stats */} + <div className="flex items-center gap-1.5 font-mono text-[10px] shrink-0"> + {file.linesAdded > 0 && ( + <span className="text-green-400">+{file.linesAdded}</span> + )} + {file.linesRemoved > 0 && ( + <span className="text-red-400">-{file.linesRemoved}</span> + )} + </div> + </div> + ); + })} + </div> + + {/* Show more/less button */} + {files.length > 10 && ( + <div className="px-3 py-2 border-t border-[rgba(117,170,252,0.1)]"> + <button + onClick={() => setExpanded(!expanded)} + className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors" + > + {expanded ? `Show less` : `Show ${files.length - 10} more files...`} + </button> + </div> + )} + </div> + ); +} |
