diff options
Diffstat (limited to 'makima/frontend/src/components/history/ResumeControls.tsx')
| -rw-r--r-- | makima/frontend/src/components/history/ResumeControls.tsx | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/makima/frontend/src/components/history/ResumeControls.tsx b/makima/frontend/src/components/history/ResumeControls.tsx new file mode 100644 index 0000000..23493f0 --- /dev/null +++ b/makima/frontend/src/components/history/ResumeControls.tsx @@ -0,0 +1,306 @@ +import { useState } from "react"; +import type { TaskCheckpoint } from "../../lib/api"; +import { rewindTask, resumeSupervisor, rewindSupervisorConversation } from "../../lib/api"; + +interface ResumeControlsProps { + taskId: string; + contractId: string | null; + checkpoints: TaskCheckpoint[]; + onActionComplete: () => void; +} + +export function ResumeControls({ + taskId, + contractId, + checkpoints, + onActionComplete, +}: ResumeControlsProps) { + const [showRewindDialog, setShowRewindDialog] = useState(false); + const [showSupervisorDialog, setShowSupervisorDialog] = useState(false); + const [selectedCheckpoint, setSelectedCheckpoint] = useState<string>(""); + const [preserveMode, setPreserveMode] = useState<"discard" | "create_branch">("create_branch"); + const [branchName, setBranchName] = useState(""); + const [resumeMode, setResumeMode] = useState<"continue" | "restart_phase">("continue"); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const handleRewindTask = async () => { + if (!selectedCheckpoint) { + setError("Select a checkpoint"); + return; + } + + setIsLoading(true); + setError(null); + try { + await rewindTask(taskId, { + checkpointId: selectedCheckpoint, + preserveMode, + branchName: preserveMode === "create_branch" ? branchName || undefined : undefined, + }); + setShowRewindDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to rewind task"); + } finally { + setIsLoading(false); + } + }; + + const handleResumeSupervisor = async () => { + if (!contractId) return; + + setIsLoading(true); + setError(null); + try { + await resumeSupervisor(contractId, { + resumeMode, + }); + setShowSupervisorDialog(false); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to resume supervisor"); + } finally { + setIsLoading(false); + } + }; + + const handleRewindConversation = async () => { + if (!contractId) return; + + setIsLoading(true); + setError(null); + try { + await rewindSupervisorConversation(contractId, { + byMessageCount: 1, // Rewind by 1 message + }); + onActionComplete(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to rewind conversation"); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + <div className="shrink-0 p-3 border-t border-[rgba(117,170,252,0.15)] bg-[rgba(0,0,0,0.2)] flex items-center gap-2"> + {/* Task controls */} + {checkpoints.length > 0 && ( + <button + onClick={() => setShowRewindDialog(true)} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-yellow-400 border border-yellow-400/30 hover:bg-yellow-400/10 transition-colors" + > + Rewind Code + </button> + )} + + {/* Supervisor controls */} + {contractId && ( + <> + <button + onClick={() => setShowSupervisorDialog(true)} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-cyan-400 border border-cyan-400/30 hover:bg-cyan-400/10 transition-colors" + > + Resume Supervisor + </button> + <button + onClick={handleRewindConversation} + disabled={isLoading} + className="px-3 py-1.5 font-mono text-[10px] uppercase text-orange-400 border border-orange-400/30 hover:bg-orange-400/10 transition-colors disabled:opacity-50" + > + Undo Last Message + </button> + </> + )} + </div> + + {/* Rewind Task Dialog */} + {showRewindDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Rewind Task Code + </h2> + <button + onClick={() => setShowRewindDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Checkpoint + </label> + <select + value={selectedCheckpoint} + onChange={(e) => setSelectedCheckpoint(e.target.value)} + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none" + > + <option value="">Select checkpoint...</option> + {checkpoints.map((cp) => ( + <option key={cp.id} value={cp.id}> + #{cp.checkpointNumber} - {cp.message || cp.commitSha.slice(0, 7)} + </option> + ))} + </select> + </div> + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Preserve Current Code + </label> + <div className="flex gap-4"> + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="radio" + name="preserveMode" + checked={preserveMode === "create_branch"} + onChange={() => setPreserveMode("create_branch")} + className="text-[#3f6fb3]" + /> + <span className="font-mono text-xs text-[#9bc3ff]">Create branch</span> + </label> + <label className="flex items-center gap-2 cursor-pointer"> + <input + type="radio" + name="preserveMode" + checked={preserveMode === "discard"} + onChange={() => setPreserveMode("discard")} + className="text-[#3f6fb3]" + /> + <span className="font-mono text-xs text-[#9bc3ff]">Discard</span> + </label> + </div> + </div> + + {preserveMode === "create_branch" && ( + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1"> + Branch Name (optional) + </label> + <input + type="text" + value={branchName} + onChange={(e) => setBranchName(e.target.value)} + placeholder="Auto-generated if empty" + className="w-full font-mono text-xs text-[#9bc3ff] bg-[#0a1525] border border-[rgba(117,170,252,0.25)] px-3 py-2 focus:border-[#3f6fb3] focus:outline-none" + /> + </div> + )} + + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowRewindDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleRewindTask} + disabled={isLoading || !selectedCheckpoint} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-yellow-600 border border-yellow-500 hover:bg-yellow-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Rewinding..." : "Rewind"} + </button> + </div> + </div> + </div> + </div> + )} + + {/* Resume Supervisor Dialog */} + {showSupervisorDialog && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-lg w-full mx-4"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="font-mono text-sm uppercase tracking-wide text-[#9bc3ff]"> + Resume Supervisor + </h2> + <button + onClick={() => setShowSupervisorDialog(false)} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 space-y-4"> + {error && ( + <div className="p-2 bg-red-400/10 border border-red-400/30 font-mono text-xs text-red-400"> + {error} + </div> + )} + + <div> + <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-2"> + Resume Mode + </label> + <div className="space-y-2"> + <label className="flex items-start gap-2 cursor-pointer p-2 border border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.25)] transition-colors"> + <input + type="radio" + name="resumeMode" + checked={resumeMode === "continue"} + onChange={() => setResumeMode("continue")} + className="mt-0.5 text-[#3f6fb3]" + /> + <div> + <span className="font-mono text-xs text-[#9bc3ff]">Continue</span> + <p className="font-mono text-[10px] text-[#7788aa] mt-0.5"> + Resume with existing conversation context + </p> + </div> + </label> + <label className="flex items-start gap-2 cursor-pointer p-2 border border-[rgba(117,170,252,0.15)] hover:border-[rgba(117,170,252,0.25)] transition-colors"> + <input + type="radio" + name="resumeMode" + checked={resumeMode === "restart_phase"} + onChange={() => setResumeMode("restart_phase")} + className="mt-0.5 text-[#3f6fb3]" + /> + <div> + <span className="font-mono text-xs text-[#9bc3ff]">Restart Phase</span> + <p className="font-mono text-[10px] text-[#7788aa] mt-0.5"> + Clear conversation but keep phase progress + </p> + </div> + </label> + </div> + </div> + + <div className="flex justify-end gap-2"> + <button + onClick={() => setShowSupervisorDialog(false)} + className="px-4 py-2 font-mono text-xs uppercase text-[#7788aa] border border-[rgba(117,170,252,0.25)] hover:text-[#9bc3ff] transition-colors" + > + Cancel + </button> + <button + onClick={handleResumeSupervisor} + disabled={isLoading} + className="px-4 py-2 font-mono text-xs uppercase text-white bg-cyan-600 border border-cyan-500 hover:bg-cyan-500 transition-colors disabled:opacity-50" + > + {isLoading ? "Resuming..." : "Resume"} + </button> + </div> + </div> + </div> + </div> + )} + </> + ); +} |
