From 205ab8a223ddf6591a3e8bfc9108506502977c11 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Fri, 16 Jan 2026 12:23:49 +0000
Subject: Fixup: use default api.makima.jp URL and fix default branch detection

Also add checkpointing/history
---
 .../src/components/history/CheckpointCard.tsx      | 284 +++++++++++++++++++++
 1 file changed, 284 insertions(+)
 create mode 100644 makima/frontend/src/components/history/CheckpointCard.tsx

(limited to 'makima/frontend/src/components/history/CheckpointCard.tsx')

diff --git a/makima/frontend/src/components/history/CheckpointCard.tsx b/makima/frontend/src/components/history/CheckpointCard.tsx
new file mode 100644
index 0000000..fee5bdc
--- /dev/null
+++ b/makima/frontend/src/components/history/CheckpointCard.tsx
@@ -0,0 +1,284 @@
+import { useState } from "react";
+import type { TaskCheckpoint } from "../../lib/api";
+import { forkTask, resumeFromCheckpoint } from "../../lib/api";
+
+interface CheckpointCardProps {
+  checkpoint: TaskCheckpoint;
+  taskId: string;
+  onActionComplete: () => void;
+}
+
+export function CheckpointCard({ checkpoint, taskId, onActionComplete }: CheckpointCardProps) {
+  const [showActions, setShowActions] = useState(false);
+  const [showForkDialog, setShowForkDialog] = useState(false);
+  const [showResumeDialog, setShowResumeDialog] = useState(false);
+  const [forkName, setForkName] = useState(`Fork from checkpoint ${checkpoint.checkpointNumber}`);
+  const [forkPlan, setForkPlan] = useState("");
+  const [resumePlan, setResumePlan] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  const handleFork = async () => {
+    if (!forkName.trim() || !forkPlan.trim()) {
+      setError("Name and plan are required");
+      return;
+    }
+
+    setIsLoading(true);
+    setError(null);
+    try {
+      await forkTask(taskId, {
+        forkFromType: "checkpoint",
+        forkFromValue: String(checkpoint.checkpointNumber),
+        newTaskName: forkName,
+        newTaskPlan: forkPlan,
+        includeConversation: true,
+      });
+      setShowForkDialog(false);
+      onActionComplete();
+    } catch (e) {
+      setError(e instanceof Error ? e.message : "Failed to fork task");
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleResume = async () => {
+    if (!resumePlan.trim()) {
+      setError("Plan is required");
+      return;
+    }
+
+    setIsLoading(true);
+    setError(null);
+    try {
+      await resumeFromCheckpoint(taskId, checkpoint.id, {
+        plan: resumePlan,
+        includeConversation: true,
+      });
+      setShowResumeDialog(false);
+      onActionComplete();
+    } catch (e) {
+      setError(e instanceof Error ? e.message : "Failed to resume from checkpoint");
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <>
+      <div className="p-3 hover:bg-[rgba(117,170,252,0.05)] transition-colors">
+        <div className="flex items-start justify-between gap-4">
+          {/* Checkpoint info */}
+          <div className="flex-1 min-w-0">
+            <div className="flex items-center gap-2">
+              <span className="font-mono text-sm text-purple-400">#{checkpoint.checkpointNumber}</span>
+              <span className="font-mono text-[10px] text-[#7788aa]">
+                {checkpoint.commitSha.slice(0, 7)}
+              </span>
+              <span className="font-mono text-[10px] text-[#556677]">
+                on {checkpoint.branchName}
+              </span>
+            </div>
+
+            {checkpoint.message && (
+              <div className="font-mono text-xs text-[#9bc3ff] mt-1">{checkpoint.message}</div>
+            )}
+
+            {/* Files changed */}
+            {checkpoint.filesChanged.length > 0 && (
+              <div className="mt-2 flex flex-wrap gap-2">
+                {checkpoint.filesChanged.slice(0, 5).map((file, i) => (
+                  <span
+                    key={i}
+                    className={`font-mono text-[9px] px-1.5 py-0.5 border ${
+                      file.action === "A"
+                        ? "text-green-400 border-green-400/30"
+                        : file.action === "D"
+                        ? "text-red-400 border-red-400/30"
+                        : "text-yellow-400 border-yellow-400/30"
+                    }`}
+                  >
+                    {file.action} {file.path.split("/").pop()}
+                  </span>
+                ))}
+                {checkpoint.filesChanged.length > 5 && (
+                  <span className="font-mono text-[9px] text-[#556677]">
+                    +{checkpoint.filesChanged.length - 5} more
+                  </span>
+                )}
+              </div>
+            )}
+
+            {/* Stats */}
+            <div className="mt-2 flex items-center gap-4 font-mono text-[9px] text-[#556677]">
+              <span className="text-green-400">+{checkpoint.linesAdded}</span>
+              <span className="text-red-400">-{checkpoint.linesRemoved}</span>
+              <span>{new Date(checkpoint.createdAt).toLocaleString()}</span>
+            </div>
+          </div>
+
+          {/* Actions button */}
+          <button
+            onClick={() => setShowActions(!showActions)}
+            className="shrink-0 p-1 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="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"
+              />
+            </svg>
+          </button>
+        </div>
+
+        {/* Actions dropdown */}
+        {showActions && (
+          <div className="mt-3 flex gap-2">
+            <button
+              onClick={() => {
+                setShowForkDialog(true);
+                setShowActions(false);
+              }}
+              className="px-3 py-1.5 font-mono text-[10px] uppercase text-purple-400 border border-purple-400/30 hover:bg-purple-400/10 transition-colors"
+            >
+              Fork from here
+            </button>
+            <button
+              onClick={() => {
+                setShowResumeDialog(true);
+                setShowActions(false);
+              }}
+              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 from here
+            </button>
+          </div>
+        )}
+      </div>
+
+      {/* Fork dialog */}
+      {showForkDialog && (
+        <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]">
+                Fork from Checkpoint #{checkpoint.checkpointNumber}
+              </h2>
+              <button
+                onClick={() => setShowForkDialog(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">
+                  New Task Name
+                </label>
+                <input
+                  type="text"
+                  value={forkName}
+                  onChange={(e) => setForkName(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"
+                />
+              </div>
+              <div>
+                <label className="block font-mono text-[10px] text-[#7788aa] uppercase mb-1">
+                  Plan for New Task
+                </label>
+                <textarea
+                  value={forkPlan}
+                  onChange={(e) => setForkPlan(e.target.value)}
+                  rows={4}
+                  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 resize-none"
+                  placeholder="Describe what this forked task should accomplish..."
+                />
+              </div>
+              <div className="flex justify-end gap-2">
+                <button
+                  onClick={() => setShowForkDialog(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={handleFork}
+                  disabled={isLoading}
+                  className="px-4 py-2 font-mono text-xs uppercase text-white bg-purple-600 border border-purple-500 hover:bg-purple-500 transition-colors disabled:opacity-50"
+                >
+                  {isLoading ? "Creating..." : "Create Fork"}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* Resume dialog */}
+      {showResumeDialog && (
+        <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 from Checkpoint #{checkpoint.checkpointNumber}
+              </h2>
+              <button
+                onClick={() => setShowResumeDialog(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">
+                  Plan for Resumed Task
+                </label>
+                <textarea
+                  value={resumePlan}
+                  onChange={(e) => setResumePlan(e.target.value)}
+                  rows={4}
+                  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 resize-none"
+                  placeholder="Describe what the resumed task should do..."
+                />
+              </div>
+              <div className="flex justify-end gap-2">
+                <button
+                  onClick={() => setShowResumeDialog(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={handleResume}
+                  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 ? "Creating..." : "Resume Task"}
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      )}
+    </>
+  );
+}
-- 
cgit v1.2.3

