summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/history/CheckpointCard.tsx
blob: fee5bdc5bc75a92049a58e5548a472b0586685c5 (plain) (tree)



























































































































































































































































































                                                                                                                                                                                         
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>
      )}
    </>
  );
}