summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/history/ResumeControls.tsx
blob: 23493f0f1dba6ea50a57a8b95586b33c0a9e4ab2 (plain) (tree)

















































































































































































































































































































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