summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/GitActionsPanel.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-26 22:12:57 +0000
committerGitHub <noreply@github.com>2026-01-26 22:12:57 +0000
commitd1f5dadb549d499c5aeee9cacf6c9aa0a233c198 (patch)
treea47e3d68a6b25bc39044a52b63099a199dce677d /makima/frontend/src/components/mesh/GitActionsPanel.tsx
parentbc1ce8013bc36a1585be05b928f2386ab56529c2 (diff)
downloadsoryu-d1f5dadb549d499c5aeee9cacf6c9aa0a233c198.tar.gz
soryu-d1f5dadb549d499c5aeee9cacf6c9aa0a233c198.zip
Add local-only mode for contracts with patch export support (#34)
* Add local_only flag to contracts database and models Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * Skip automatic completion actions in local_only mode Add `local_only` flag to contracts that prevents automatic completion actions (branch, merge, pr) from executing when tasks complete. This allows users to manually handle code changes via patch files or other means when operating in local-only mode. Changes: - Add `local_only` field to Contract model and request types - Add database migration for the new column - Add `local_only` parameter to SpawnTask command in both state.rs and daemon protocol.rs - Modify task manager to skip completion action execution when `local_only` is true, with appropriate logging - Pass `local_only` flag through all task spawning paths: - mesh_supervisor.rs (task spawn, retry, resume) - mesh.rs (task start, reassign, continue) - mesh_chat.rs (run task) - contract_chat.rs (run task) - Update repository create/update functions to handle `local_only` Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * Implement core patch export system Add functionality to create uncompressed, human-readable git patches for export. This enables users to generate patches that can be manually applied or shared, without the compression used for internal checkpoints. Changes: - Add ExportPatchResult struct with patch content, file count, and line stats - Add create_export_patch() function that generates diffs against a base SHA - Add get_head_sha() utility function - Add parse_diff_stat() helper to extract line counts from git output - Add CreateExportPatch command to daemon protocol - Add ExportPatchCreated response message to protocol - Add handler in task manager to process export patch requests - Add server-side handling to broadcast patch results to UI The export patch system automatically finds the merge-base when no base SHA is provided, trying upstream tracking branch first, then common default branches (origin/main, origin/master, main, master). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * Add GitActionsPanel frontend component * Add WorktreeFilesPanel and PatchesListPanel components * Add local-only mode toggle to contract creation --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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];
+}