From 2ee09745c12f0762ab99fadb00cdc0c363cb027b Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 26 Jan 2026 19:15:03 +0000 Subject: [WIP] Heartbeat checkpoint - 2026-01-26 19:15:03 UTC --- .../src/components/mesh/GitActionsPanel.tsx | 181 +++++++++++++++++++++ makima/frontend/src/components/mesh/TaskDetail.tsx | 13 ++ 2 files changed, 194 insertions(+) create mode 100644 makima/frontend/src/components/mesh/GitActionsPanel.tsx (limited to 'makima/frontend/src/components/mesh') 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(null); + const [exportedPatch, setExportedPatch] = useState(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 ( +
+ {/* Section Header */} +
+ Git Actions +
+ + {/* Local-only mode alert */} + {isLocalOnly && ( +
+ + Contract is in local-only mode. Push/PR actions disabled. +
+ )} + + {/* Toast notification */} + {toast && ( +
+ {toast.message} +
+ )} + + {/* Action buttons */} +
+ + + + + +
+ + {/* Export result info */} + {exportedPatch && ( +
+
Exported Patch
+
+ File: {exportedPatch.fileName} +
+ {exportedPatch.filePath && ( +
+ Path: {exportedPatch.filePath} +
+ )} + {exportedPatch.patchSize && ( +
+ Size: {formatBytes(exportedPatch.patchSize)} +
+ )} +
+ )} +
+ ); +} + +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]; +} diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx index 8936d28..2822b6d 100644 --- a/makima/frontend/src/components/mesh/TaskDetail.tsx +++ b/makima/frontend/src/components/mesh/TaskDetail.tsx @@ -7,6 +7,7 @@ import { PRPreview } from "./PRPreview"; import { InlineSubtaskEditor } from "./InlineSubtaskEditor"; import { DirectoryInput } from "./DirectoryInput"; import { BranchTaskModal } from "./BranchTaskModal"; +import { GitActionsPanel } from "./GitActionsPanel"; interface TaskDetailProps { task: TaskWithSubtasks; @@ -36,6 +37,8 @@ interface TaskDetailProps { fetchSubtasks?: (taskId: string) => Promise; /** For supervisor tasks: all tasks in the contract (excluding the supervisor itself) */ contractTasks?: TaskSummary[]; + /** Whether the contract is in local-only mode (no push/PR) */ + isLocalOnly?: boolean; } function formatDate(dateStr: string): string { @@ -119,6 +122,7 @@ export function TaskDetail({ onAutoMerge, fetchSubtasks, contractTasks, + isLocalOnly = false, }: TaskDetailProps) { const [isEditing, setIsEditing] = useState(false); const [editName, setEditName] = useState(task.name); @@ -879,6 +883,15 @@ export function TaskDetail({ )} )} + + {/* Git Actions Panel for manual git operations */} + {task.status === "done" && ( + + )} {/* Overlay Diff Modal */} -- cgit v1.2.3