summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/contracts/CommandModePanel.tsx
blob: b39b30912d3af161c6fb65658e48c9ed66a13ba8 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                              
                                           





                                                           
                 


                        
                                 












































                                                   

                                                                                 



                                                          


                                                  
                                                             

                                                    











                                                               
                                                                                














                                                              
                                                                               
















                                                                      
                                                                                 




                                                                 
















                                                                                        



                                                                   
                      
             














                                                                                                                                                                    




                                                         
                                                                             
                                                     
                                                                                                    
                                                     
                                                                                    
                                                    
                                                                    
                                                    
                                                                       
                                                  
                                                                 
             
                                           















                                                                                                                                                                                                     
                                                             








                                                                                                                                                                                                  
                                                             








                                                                                                                                                                                                        
                                                            



                   































                                                                                                                                                                                                        




                                                                                                            
                                            







                                                                                                            
                                        





                 
import { useState, useCallback } from "react";
import { useNavigate } from "react-router";
import type { ContractWithRelations } from "../../lib/api";
import {
  getSupervisorStatus,
  startSupervisor,
  stopSupervisor,
  resumeSupervisor,
  updateContract,
  type SupervisorStatus,
} from "../../lib/api";

interface CommandModePanelProps {
  contract: ContractWithRelations;
  onUpdate: () => void;
}

const statusConfig: Record<
  SupervisorStatus["status"],
  { label: string; color: string; bgColor: string }
> = {
  not_configured: {
    label: "Not Configured",
    color: "text-[#555]",
    bgColor: "bg-[#555]/10",
  },
  pending: {
    label: "Ready",
    color: "text-yellow-400",
    bgColor: "bg-yellow-400/10",
  },
  starting: {
    label: "Starting...",
    color: "text-blue-400",
    bgColor: "bg-blue-400/10",
  },
  running: {
    label: "Running",
    color: "text-green-400",
    bgColor: "bg-green-400/10",
  },
  paused: {
    label: "Paused",
    color: "text-orange-400",
    bgColor: "bg-orange-400/10",
  },
  done: {
    label: "Completed",
    color: "text-blue-400",
    bgColor: "bg-blue-400/10",
  },
  failed: {
    label: "Failed",
    color: "text-red-400",
    bgColor: "bg-red-400/10",
  },
};

export function CommandModePanel({ contract, onUpdate }: CommandModePanelProps) {
  const navigate = useNavigate();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const supervisorStatus = getSupervisorStatus(contract);

  const handleGoToSupervisor = useCallback(() => {
    if (supervisorStatus.supervisorTaskId) {
      navigate(`/exec/${supervisorStatus.supervisorTaskId}`);
    }
  }, [supervisorStatus.supervisorTaskId, navigate]);
  const config = statusConfig[supervisorStatus.status];

  const handleStart = useCallback(async () => {
    if (!supervisorStatus.supervisorTaskId) return;

    setLoading(true);
    setError(null);

    try {
      await startSupervisor(supervisorStatus.supervisorTaskId);
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to start command mode");
    } finally {
      setLoading(false);
    }
  }, [supervisorStatus.supervisorTaskId, onUpdate]);

  const handleStop = useCallback(async () => {
    if (!supervisorStatus.supervisorTaskId) return;

    setLoading(true);
    setError(null);

    try {
      await stopSupervisor(supervisorStatus.supervisorTaskId);
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to stop command mode");
    } finally {
      setLoading(false);
    }
  }, [supervisorStatus.supervisorTaskId, onUpdate]);

  const handleResume = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      await resumeSupervisor(contract.id, { resumeMode: "continue" });
      // After resuming, we need to start the task
      if (supervisorStatus.supervisorTaskId) {
        await startSupervisor(supervisorStatus.supervisorTaskId);
      }
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to resume command mode");
    } finally {
      setLoading(false);
    }
  }, [contract.id, supervisorStatus.supervisorTaskId, onUpdate]);

  const handlePhaseGuardChange = useCallback(async (enabled: boolean) => {
    setLoading(true);
    setError(null);

    try {
      await updateContract(contract.id, {
        phaseGuard: enabled,
        version: contract.version,
      });
      onUpdate();
    } catch (e) {
      setError(e instanceof Error ? e.message : "Failed to update phase guard setting");
    } finally {
      setLoading(false);
    }
  }, [contract.id, contract.version, onUpdate]);

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <h3 className="font-mono text-xs text-[#75aafc] uppercase">
          Command Mode
        </h3>
        <div className="flex items-center gap-2">
          {supervisorStatus.supervisorTaskId && (
            <button
              onClick={handleGoToSupervisor}
              className="px-2 py-1 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] hover:bg-[rgba(117,170,252,0.1)] transition-colors flex items-center gap-1"
            >
              <span className="text-[#75aafc]">▶</span>
              Supervisor
            </button>
          )}
          <div
            className={`px-2 py-1 rounded font-mono text-xs ${config.color} ${config.bgColor}`}
          >
            {config.label}
          </div>
        </div>
      </div>

      <p className="font-mono text-xs text-[#555]">
        {supervisorStatus.status === "not_configured" ? (
          "This contract does not have a Command Mode supervisor configured."
        ) : supervisorStatus.status === "running" ? (
          "Command Mode is actively working on this contract, spawning tasks and managing progress."
        ) : supervisorStatus.status === "pending" ? (
          "Command Mode is ready to start. Click 'Enable' to begin autonomous work."
        ) : supervisorStatus.status === "paused" ? (
          "Command Mode is paused. Click 'Resume' to continue work."
        ) : supervisorStatus.status === "failed" ? (
          "Command Mode encountered an error. You can resume to retry."
        ) : supervisorStatus.status === "done" ? (
          "Command Mode has completed its work on this contract."
        ) : (
          "Command Mode is initializing..."
        )}
      </p>

      {error && (
        <div className="px-3 py-2 bg-red-500/10 border border-red-400/30 font-mono text-xs text-red-400">
          {error}
        </div>
      )}

      <div className="flex gap-2">
        {supervisorStatus.canStart && (
          <button
            onClick={handleStart}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-green-600/20 border border-green-400/50 hover:bg-green-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Starting..." : "Enable Command Mode"}
          </button>
        )}

        {supervisorStatus.canResume && (
          <button
            onClick={handleResume}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-blue-600/20 border border-blue-400/50 hover:bg-blue-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Resuming..." : "Resume Command Mode"}
          </button>
        )}

        {supervisorStatus.canStop && (
          <button
            onClick={handleStop}
            disabled={loading}
            className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-orange-600/20 border border-orange-400/50 hover:bg-orange-600/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {loading ? "Stopping..." : "Pause Command Mode"}
          </button>
        )}
      </div>

      {/* Phase Guard Toggle */}
      <div className="pt-3 border-t border-dashed border-[rgba(117,170,252,0.2)]">
        <label className="flex items-start gap-3 cursor-pointer group">
          <div className="relative mt-0.5">
            <input
              type="checkbox"
              checked={contract.phaseGuard ?? false}
              onChange={(e) => handlePhaseGuardChange(e.target.checked)}
              disabled={loading}
              className="sr-only peer"
            />
            <div className="w-9 h-5 bg-[rgba(117,170,252,0.1)] border border-[rgba(117,170,252,0.3)] rounded-full peer-checked:bg-[rgba(117,170,252,0.3)] transition-colors peer-disabled:opacity-50" />
            <div className="absolute left-0.5 top-0.5 w-4 h-4 bg-[#555] rounded-full transition-transform peer-checked:translate-x-4 peer-checked:bg-[#75aafc] peer-disabled:opacity-50" />
          </div>
          <div className="flex-1">
            <div className="flex items-center gap-2">
              <span className="font-mono text-sm text-[#dbe7ff] group-hover:text-white transition-colors">
                Phase Guard
              </span>
              {contract.phaseGuard && (
                <span className="px-1.5 py-0.5 text-[9px] font-mono uppercase bg-yellow-500/20 text-yellow-400 border border-yellow-400/30 rounded">
                  active
                </span>
              )}
            </div>
            <div className="font-mono text-xs text-[#555] mt-0.5">
              Ask for confirmation before advancing to the next phase
            </div>
          </div>
        </label>
      </div>

      {/* Show running indicator when active */}
      {supervisorStatus.status === "running" && (
        <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
          <span className="font-mono text-xs text-green-400">
            Command Mode is actively working
          </span>
        </div>
      )}

      {supervisorStatus.status === "starting" && (
        <div className="flex items-center gap-2 pt-2 border-t border-dashed border-[rgba(117,170,252,0.2)]">
          <div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
          <span className="font-mono text-xs text-blue-400">
            Initializing command mode...
          </span>
        </div>
      )}
    </div>
  );
}