summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/GitActionsPanel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh/GitActionsPanel.tsx')
-rw-r--r--makima/frontend/src/components/mesh/GitActionsPanel.tsx181
1 files changed, 181 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/GitActionsPanel.tsx b/makima/frontend/src/components/mesh/GitActionsPanel.tsx
new file mode 100644
index 0000000..be2e06d
--- /dev/null
+++ b/makima/frontend/src/components/mesh/GitActionsPanel.tsx
@@ -0,0 +1,181 @@
+import { useState, useCallback } from "react";
+import { exportTaskPatch, pushTaskBranch, createTaskPR, type ExportPatchResponse } from "../../lib/api";
+
+interface GitActionsPanelProps {
+ taskId: string;
+ isLocalOnly: boolean;
+ taskStatus: string;
+}
+
+interface ToastMessage {
+ type: "success" | "error" | "info";
+ message: string;
+}
+
+export function GitActionsPanel({
+ taskId,
+ isLocalOnly,
+ taskStatus,
+}: GitActionsPanelProps) {
+ const [isExporting, setIsExporting] = useState(false);
+ const [isPushing, setIsPushing] = useState(false);
+ const [isCreatingPR, setIsCreatingPR] = useState(false);
+ const [toast, setToast] = useState<ToastMessage | null>(null);
+ const [exportedPatch, setExportedPatch] = useState<ExportPatchResponse | null>(null);
+
+ // Only show for completed tasks
+ if (taskStatus !== "done") return null;
+
+ const showToast = (type: ToastMessage["type"], message: string) => {
+ setToast({ type, message });
+ setTimeout(() => setToast(null), 5000);
+ };
+
+ const handleExportPatch = useCallback(async () => {
+ setIsExporting(true);
+ try {
+ const result = await exportTaskPatch(taskId);
+ setExportedPatch(result);
+ showToast("success", `Patch generated: ${result.fileName}`);
+ } catch (e) {
+ showToast("error", e instanceof Error ? e.message : "Failed to generate patch");
+ } finally {
+ setIsExporting(false);
+ }
+ }, [taskId]);
+
+ const handlePushBranch = useCallback(async () => {
+ if (isLocalOnly) {
+ showToast("error", "Push disabled in local-only mode");
+ return;
+ }
+ setIsPushing(true);
+ try {
+ const result = await pushTaskBranch(taskId);
+ showToast("success", `Branch pushed: ${result.branchName}`);
+ } catch (e) {
+ showToast("error", e instanceof Error ? e.message : "Failed to push branch");
+ } finally {
+ setIsPushing(false);
+ }
+ }, [taskId, isLocalOnly]);
+
+ const handleCreatePR = useCallback(async () => {
+ if (isLocalOnly) {
+ showToast("error", "PR creation disabled in local-only mode");
+ return;
+ }
+ setIsCreatingPR(true);
+ try {
+ const result = await createTaskPR(taskId);
+ if (result.prUrl) {
+ showToast("success", `PR created: ${result.prUrl}`);
+ // Open PR in new tab
+ window.open(result.prUrl, "_blank", "noopener,noreferrer");
+ } else {
+ showToast("success", "PR creation initiated");
+ }
+ } catch (e) {
+ showToast("error", e instanceof Error ? e.message : "Failed to create PR");
+ } finally {
+ setIsCreatingPR(false);
+ }
+ }, [taskId, isLocalOnly]);
+
+ return (
+ <div className="space-y-3">
+ {/* Section Header */}
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Git Actions
+ </div>
+
+ {/* Local-only mode alert */}
+ {isLocalOnly && (
+ <div className="bg-yellow-400/10 border border-yellow-400/30 px-3 py-2 font-mono text-xs text-yellow-400 flex items-center gap-2">
+ <span className="w-1.5 h-1.5 bg-yellow-400 rounded-full" />
+ Contract is in local-only mode. Push/PR actions disabled.
+ </div>
+ )}
+
+ {/* Toast notification */}
+ {toast && (
+ <div
+ className={`px-3 py-2 font-mono text-xs border ${
+ toast.type === "success"
+ ? "bg-green-400/10 border-green-400/30 text-green-400"
+ : toast.type === "error"
+ ? "bg-red-400/10 border-red-400/30 text-red-400"
+ : "bg-blue-400/10 border-blue-400/30 text-blue-400"
+ }`}
+ >
+ {toast.message}
+ </div>
+ )}
+
+ {/* Action buttons */}
+ <div className="flex flex-wrap gap-2">
+ <button
+ onClick={handleExportPatch}
+ disabled={isExporting}
+ className="px-3 py-1.5 font-mono text-xs text-[#75aafc] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {isExporting ? "Generating..." : "Generate Patch"}
+ </button>
+
+ <button
+ onClick={handlePushBranch}
+ disabled={isPushing || isLocalOnly}
+ className={`px-3 py-1.5 font-mono text-xs border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
+ isLocalOnly
+ ? "text-[#555] border-[rgba(117,170,252,0.15)] cursor-not-allowed"
+ : "text-cyan-400 border-cyan-400/30 hover:border-cyan-400/50 hover:bg-cyan-400/10"
+ }`}
+ title={isLocalOnly ? "Disabled in local-only mode" : "Push changes to remote branch"}
+ >
+ {isPushing ? "Pushing..." : "Push Remote Branch"}
+ </button>
+
+ <button
+ onClick={handleCreatePR}
+ disabled={isCreatingPR || isLocalOnly}
+ className={`px-3 py-1.5 font-mono text-xs border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
+ isLocalOnly
+ ? "text-[#555] border-[rgba(117,170,252,0.15)] cursor-not-allowed"
+ : "text-green-400 border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10"
+ }`}
+ title={isLocalOnly ? "Disabled in local-only mode" : "Create pull request"}
+ >
+ {isCreatingPR ? "Creating PR..." : "Create PR"}
+ </button>
+ </div>
+
+ {/* Export result info */}
+ {exportedPatch && (
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-2">
+ <div className="font-mono text-xs text-[#555]">Exported Patch</div>
+ <div className="font-mono text-xs text-[#75aafc] break-all">
+ <span className="text-[#555]">File:</span> {exportedPatch.fileName}
+ </div>
+ {exportedPatch.filePath && (
+ <div className="font-mono text-xs text-[#75aafc] break-all">
+ <span className="text-[#555]">Path:</span> {exportedPatch.filePath}
+ </div>
+ )}
+ {exportedPatch.patchSize && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Size:</span> {formatBytes(exportedPatch.patchSize)}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+}