summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/history/ResumeControls.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-16 12:23:49 +0000
committersoryu <soryu@soryu.co>2026-01-16 12:23:49 +0000
commit205ab8a223ddf6591a3e8bfc9108506502977c11 (patch)
treed768063acff233dbeea223d7b6ea69d7e3038300 /makima/frontend/src/components/history/ResumeControls.tsx
parent05931d19bc0c161d0177c3f983d0cd903d5e8ae3 (diff)
downloadsoryu-205ab8a223ddf6591a3e8bfc9108506502977c11.tar.gz
soryu-205ab8a223ddf6591a3e8bfc9108506502977c11.zip
Fixup: use default api.makima.jp URL and fix default branch detection
Also add checkpointing/history
Diffstat (limited to 'makima/frontend/src/components/history/ResumeControls.tsx')
-rw-r--r--makima/frontend/src/components/history/ResumeControls.tsx306
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>
+ )}
+ </>
+ );
+}