From 94e5604e770d6589f786ea71e51738e21492f301 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Wed, 21 Jan 2026 17:31:46 +0000
Subject: Add task branching feature (#15)

---
 makima/frontend/src/components/mesh/TaskDetail.tsx | 26 ++++++++++++++++++++++
 1 file changed, 26 insertions(+)

(limited to 'makima/frontend/src/components/mesh/TaskDetail.tsx')

diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx
index efe26a8..a74f394 100644
--- a/makima/frontend/src/components/mesh/TaskDetail.tsx
+++ b/makima/frontend/src/components/mesh/TaskDetail.tsx
@@ -6,6 +6,7 @@ import { OverlayDiffViewer } from "./OverlayDiffViewer";
 import { PRPreview } from "./PRPreview";
 import { InlineSubtaskEditor } from "./InlineSubtaskEditor";
 import { DirectoryInput } from "./DirectoryInput";
+import { BranchTaskModal } from "./BranchTaskModal";
 
 interface TaskDetailProps {
   task: TaskWithSubtasks;
@@ -25,6 +26,8 @@ interface TaskDetailProps {
   viewingSubtaskId?: string | null;
   /** Navigate to view the contract */
   onViewContract?: (contractId: string) => void;
+  /** Branch the task to create a new task with same state */
+  onBranch?: (taskId: string, message: string, name?: string) => Promise<void>;
   // Optional advanced features
   overlayDiff?: string;
   changedFiles?: string[];
@@ -110,6 +113,7 @@ export function TaskDetail({
   onToggleSubtaskOutput,
   viewingSubtaskId,
   onViewContract,
+  onBranch,
   overlayDiff,
   changedFiles,
   onRequestDiff,
@@ -142,6 +146,8 @@ export function TaskDetail({
   const [isCloning, setIsCloning] = useState(false);
   const [cloneError, setCloneError] = useState<string | null>(null);
   const [cloneTargetDir, setCloneTargetDir] = useState("");
+  // Track branch modal state
+  const [showBranchModal, setShowBranchModal] = useState(false);
 
   // Check if task is running
   const isTaskRunning = task.status === "running" || task.status === "initializing" || task.status === "starting";
@@ -151,6 +157,8 @@ export function TaskDetail({
   const isSupervisor = task.isSupervisor === true;
   // Show continue for supervisors (always) or terminal states for other tasks
   const canContinue = isSupervisor || isTaskTerminal;
+  // Show branch button when task has run at least once (not pending)
+  const canBranch = onBranch && task.status !== "pending";
 
   // Determine which tasks to show: for supervisors, show contractTasks; for regular tasks, show subtasks
   const displayTasks = useMemo(() => {
@@ -380,6 +388,15 @@ export function TaskDetail({
                   Continue
                 </button>
               )}
+              {canBranch && (
+                <button
+                  onClick={() => setShowBranchModal(true)}
+                  className="px-3 py-1 font-mono text-xs text-purple-400 border border-purple-400/30 hover:border-purple-400/50 hover:bg-purple-400/10 transition-colors uppercase flex items-center gap-1"
+                >
+                  <span className="w-1.5 h-1.5 bg-purple-400 rounded-full" />
+                  Branch
+                </button>
+              )}
               <button
                 onClick={() => setIsEditing(true)}
                 className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
@@ -908,6 +925,15 @@ export function TaskDetail({
           </div>
         </div>
       )}
+
+      {/* Branch Task Modal */}
+      {showBranchModal && onBranch && (
+        <BranchTaskModal
+          task={task}
+          onBranch={onBranch}
+          onClose={() => setShowBranchModal(false)}
+        />
+      )}
     </div>
   );
 }
-- 
cgit v1.2.3

