summaryrefslogblamecommitdiff
path: root/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx
blob: b5295881951ad1a01a5568463d709fc2ee359e1a (plain) (tree)




































































































































































































                                                                                                                                                                                                   
import { useState, useEffect, useCallback } from "react";
import type { WorktreeInfo } from "../../lib/api";
import { getWorktreeInfo } from "../../lib/api";

interface WorktreeFilesPanelProps {
  taskId: string;
}

/** Get status badge styling based on file status */
function getStatusStyle(status: string): { color: string; bgColor: string; label: string } {
  switch (status) {
    case "M":
    case "modified":
      return { color: "text-yellow-400", bgColor: "bg-yellow-400/10", label: "M" };
    case "A":
    case "added":
      return { color: "text-green-400", bgColor: "bg-green-400/10", label: "A" };
    case "D":
    case "deleted":
      return { color: "text-red-400", bgColor: "bg-red-400/10", label: "D" };
    case "R":
    case "renamed":
      return { color: "text-cyan-400", bgColor: "bg-cyan-400/10", label: "R" };
    case "C":
    case "copied":
      return { color: "text-purple-400", bgColor: "bg-purple-400/10", label: "C" };
    case "U":
    case "unmerged":
      return { color: "text-orange-400", bgColor: "bg-orange-400/10", label: "U" };
    case "?":
    case "untracked":
      return { color: "text-[#555]", bgColor: "bg-[#555]/10", label: "?" };
    default:
      return { color: "text-[#9bc3ff]", bgColor: "bg-[rgba(117,170,252,0.1)]", label: status.charAt(0).toUpperCase() };
  }
}

export function WorktreeFilesPanel({ taskId }: WorktreeFilesPanelProps) {
  const [worktreeInfo, setWorktreeInfo] = useState<WorktreeInfo | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [expanded, setExpanded] = useState(false);

  const fetchWorktreeInfo = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const info = await getWorktreeInfo(taskId);
      setWorktreeInfo(info);
    } catch (e) {
      console.error("Failed to fetch worktree info:", e);
      setError(e instanceof Error ? e.message : "Failed to fetch worktree info");
    } finally {
      setLoading(false);
    }
  }, [taskId]);

  useEffect(() => {
    fetchWorktreeInfo();
  }, [fetchWorktreeInfo]);

  if (loading) {
    return (
      <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3">
        <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase mb-2">
          Worktree Changes
        </div>
        <div className="font-mono text-xs text-[#555] animate-pulse">
          Loading worktree info...
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3">
        <div className="flex items-center justify-between mb-2">
          <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
            Worktree Changes
          </div>
          <button
            onClick={fetchWorktreeInfo}
            className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
          >
            Retry
          </button>
        </div>
        <div className="font-mono text-xs text-red-400">
          {error}
        </div>
      </div>
    );
  }

  if (!worktreeInfo || worktreeInfo.files.length === 0) {
    return (
      <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3">
        <div className="flex items-center justify-between mb-2">
          <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
            Worktree Changes
          </div>
          <button
            onClick={fetchWorktreeInfo}
            className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
          >
            Refresh
          </button>
        </div>
        <div className="font-mono text-xs text-[#555]">
          No changes in worktree
        </div>
      </div>
    );
  }

  const { stats, files } = worktreeInfo;
  const displayFiles = expanded ? files : files.slice(0, 10);

  return (
    <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)]">
      {/* Header */}
      <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.1)]">
        <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
          Worktree Changes
        </div>
        <div className="flex items-center gap-3">
          {/* Stats */}
          <div className="flex items-center gap-2 font-mono text-[10px]">
            <span className="text-[#75aafc]">
              {stats.filesChanged} file{stats.filesChanged !== 1 ? "s" : ""}
            </span>
            <span className="text-green-400">+{stats.insertions}</span>
            <span className="text-red-400">-{stats.deletions}</span>
          </div>
          <button
            onClick={fetchWorktreeInfo}
            className="font-mono text-[10px] text-[#555] hover:text-[#75aafc] transition-colors"
            title="Refresh"
          >
            <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
            </svg>
          </button>
        </div>
      </div>

      {/* File list */}
      <div className="divide-y divide-[rgba(117,170,252,0.05)]">
        {displayFiles.map((file) => {
          const statusStyle = getStatusStyle(file.status);
          return (
            <div
              key={file.path}
              className="flex items-center gap-2 px-3 py-1.5 hover:bg-[rgba(117,170,252,0.03)]"
            >
              {/* Status badge */}
              <span
                className={`w-5 h-5 flex items-center justify-center font-mono text-[10px] font-medium ${statusStyle.color} ${statusStyle.bgColor} border border-current/20`}
                title={file.status}
              >
                {statusStyle.label}
              </span>

              {/* File path */}
              <span className="flex-1 font-mono text-xs text-[#dbe7ff] truncate" title={file.path}>
                {file.path}
              </span>

              {/* Line stats */}
              <div className="flex items-center gap-1.5 font-mono text-[10px] shrink-0">
                {file.linesAdded > 0 && (
                  <span className="text-green-400">+{file.linesAdded}</span>
                )}
                {file.linesRemoved > 0 && (
                  <span className="text-red-400">-{file.linesRemoved}</span>
                )}
              </div>
            </div>
          );
        })}
      </div>

      {/* Show more/less button */}
      {files.length > 10 && (
        <div className="px-3 py-2 border-t border-[rgba(117,170,252,0.1)]">
          <button
            onClick={() => setExpanded(!expanded)}
            className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
          >
            {expanded ? `Show less` : `Show ${files.length - 10} more files...`}
          </button>
        </div>
      )}
    </div>
  );
}