summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/PatchesListPanel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh/PatchesListPanel.tsx')
-rw-r--r--makima/frontend/src/components/mesh/PatchesListPanel.tsx236
1 files changed, 236 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/PatchesListPanel.tsx b/makima/frontend/src/components/mesh/PatchesListPanel.tsx
new file mode 100644
index 0000000..b63d7e7
--- /dev/null
+++ b/makima/frontend/src/components/mesh/PatchesListPanel.tsx
@@ -0,0 +1,236 @@
+import { useState, useEffect, useCallback } from "react";
+import type { PatchSummary } from "../../lib/api";
+import { listTaskPatches } from "../../lib/api";
+
+interface PatchesListPanelProps {
+ taskId: string;
+ contractId: string;
+}
+
+/** Format a date for display */
+function formatDate(dateStr: string): string {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+/** Copy text to clipboard and show feedback */
+async function copyToClipboard(text: string): Promise<boolean> {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ // Fallback for older browsers
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ textArea.style.position = "fixed";
+ textArea.style.left = "-999999px";
+ textArea.style.top = "-999999px";
+ document.body.appendChild(textArea);
+ textArea.select();
+ try {
+ document.execCommand("copy");
+ return true;
+ } catch {
+ return false;
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ }
+}
+
+export function PatchesListPanel({ taskId, contractId }: PatchesListPanelProps) {
+ const [patches, setPatches] = useState<PatchSummary[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const [copiedPatchId, setCopiedPatchId] = useState<string | null>(null);
+ const [expandedPatchId, setExpandedPatchId] = useState<string | null>(null);
+
+ const fetchPatches = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const patchList = await listTaskPatches(taskId, contractId);
+ setPatches(patchList);
+ } catch (e) {
+ console.error("Failed to fetch patches:", e);
+ setError(e instanceof Error ? e.message : "Failed to fetch patches");
+ } finally {
+ setLoading(false);
+ }
+ }, [taskId, contractId]);
+
+ useEffect(() => {
+ fetchPatches();
+ }, [fetchPatches]);
+
+ const handleCopyApplyCommand = useCallback(async (patch: PatchSummary) => {
+ // Generate the apply command for this patch
+ const command = `makima patch apply ${patch.id}`;
+ const success = await copyToClipboard(command);
+ if (success) {
+ setCopiedPatchId(patch.id);
+ setTimeout(() => setCopiedPatchId(null), 2000);
+ }
+ }, []);
+
+ const handleViewPatch = useCallback((patchId: string) => {
+ setExpandedPatchId(expandedPatchId === patchId ? null : patchId);
+ }, [expandedPatchId]);
+
+ 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">
+ Exported Patches
+ </div>
+ <div className="font-mono text-xs text-[#555] animate-pulse">
+ Loading patches...
+ </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">
+ Exported Patches
+ </div>
+ <button
+ onClick={fetchPatches}
+ 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 (patches.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">
+ Exported Patches
+ </div>
+ <button
+ onClick={fetchPatches}
+ className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
+ >
+ Refresh
+ </button>
+ </div>
+ <div className="font-mono text-xs text-[#555]">
+ No patches exported yet
+ </div>
+ </div>
+ );
+ }
+
+ 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">
+ Exported Patches
+ </div>
+ <div className="flex items-center gap-2">
+ <span className="font-mono text-[10px] text-[#75aafc]">
+ {patches.length} patch{patches.length !== 1 ? "es" : ""}
+ </span>
+ <button
+ onClick={fetchPatches}
+ 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>
+
+ {/* Patch list */}
+ <div className="divide-y divide-[rgba(117,170,252,0.1)]">
+ {patches.map((patch) => (
+ <div
+ key={patch.id}
+ className="p-3"
+ >
+ {/* Patch header */}
+ <div className="flex items-start justify-between gap-2 mb-2">
+ <div className="flex-1 min-w-0">
+ <div className="font-mono text-sm text-[#dbe7ff] truncate" title={patch.name}>
+ {patch.name}
+ </div>
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1 font-mono text-[10px] text-[#555]">
+ <span title={new Date(patch.createdAt).toLocaleString()}>
+ {formatDate(patch.createdAt)}
+ </span>
+ <span>
+ {patch.filesCount} file{patch.filesCount !== 1 ? "s" : ""}
+ </span>
+ <span className="text-green-400">+{patch.linesAdded}</span>
+ <span className="text-red-400">-{patch.linesRemoved}</span>
+ </div>
+ </div>
+ </div>
+
+ {/* Action buttons */}
+ <div className="flex flex-wrap gap-2">
+ <button
+ onClick={() => handleViewPatch(patch.id)}
+ className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors"
+ >
+ {expandedPatchId === patch.id ? "Hide" : "View"}
+ </button>
+ <button
+ onClick={() => handleCopyApplyCommand(patch)}
+ className={`px-2 py-1 font-mono text-[10px] border transition-colors ${
+ copiedPatchId === patch.id
+ ? "text-green-400 border-green-400/30 bg-green-400/10"
+ : "text-[#9bc3ff] border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)]"
+ }`}
+ >
+ {copiedPatchId === patch.id ? "Copied!" : "Copy Apply Command"}
+ </button>
+ </div>
+
+ {/* Expanded patch content */}
+ {expandedPatchId === patch.id && patch.description && (
+ <div className="mt-3 p-2 bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.1)]">
+ <pre className="font-mono text-[10px] text-[#75aafc] whitespace-pre-wrap overflow-x-auto">
+ {patch.description}
+ </pre>
+ </div>
+ )}
+
+ {/* Expanded - show files if available */}
+ {expandedPatchId === patch.id && patch.files && patch.files.length > 0 && (
+ <div className="mt-2">
+ <div className="font-mono text-[10px] text-[#555] mb-1">Changed files:</div>
+ <div className="space-y-0.5">
+ {patch.files.map((file, idx) => (
+ <div key={idx} className="font-mono text-[10px] text-[#75aafc] pl-2">
+ {file}
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+}