summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/TaskDetail.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/mesh/TaskDetail.tsx')
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx886
1 files changed, 886 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx
new file mode 100644
index 0000000..be4fb80
--- /dev/null
+++ b/makima/frontend/src/components/mesh/TaskDetail.tsx
@@ -0,0 +1,886 @@
+import { useState, useCallback, useMemo, useEffect } from "react";
+import type { TaskWithSubtasks, TaskStatus, TaskSummary, CompletionAction, DaemonDirectory } from "../../lib/api";
+import { retryCompletionAction, getDaemonDirectories, cloneWorktree } from "../../lib/api";
+import { SubtaskTree, SubtaskProgressBar, calculateTreeStats } from "./SubtaskTree";
+import { OverlayDiffViewer } from "./OverlayDiffViewer";
+import { PRPreview } from "./PRPreview";
+import { InlineSubtaskEditor } from "./InlineSubtaskEditor";
+import { DirectoryInput } from "./DirectoryInput";
+
+interface TaskDetailProps {
+ task: TaskWithSubtasks;
+ loading: boolean;
+ onBack: () => void;
+ onSave: (taskId: string, name: string, description: string, plan: string, targetRepoPath?: string, completionAction?: CompletionAction) => void;
+ onDelete: (taskId: string) => void;
+ onStart: (taskId: string) => void;
+ onStop: (taskId: string) => void;
+ onRestart: (taskId: string) => void;
+ onContinue: (taskId: string) => void;
+ onSelectSubtask: (taskId: string) => void;
+ onCreateSubtask: () => void;
+ /** Toggle viewing a subtask's output (for running subtasks) */
+ onToggleSubtaskOutput?: (subtaskId: string, subtaskName: string) => void;
+ /** Which subtask's output is currently being viewed */
+ viewingSubtaskId?: string | null;
+ // Optional advanced features
+ overlayDiff?: string;
+ changedFiles?: string[];
+ onRequestDiff?: () => void;
+ onCreatePR?: (title: string, body: string, draft: boolean) => Promise<void>;
+ onAutoMerge?: () => Promise<void>;
+ fetchSubtasks?: (taskId: string) => Promise<TaskSummary[]>;
+}
+
+function formatDate(dateStr: string): string {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+function getStatusColor(status: TaskStatus): string {
+ switch (status) {
+ case "pending":
+ return "text-[#9bc3ff]";
+ case "initializing":
+ case "starting":
+ return "text-cyan-400";
+ case "running":
+ return "text-green-400";
+ case "paused":
+ return "text-yellow-400";
+ case "blocked":
+ return "text-orange-400";
+ case "done":
+ return "text-emerald-400";
+ case "failed":
+ return "text-red-400";
+ case "merged":
+ return "text-purple-400";
+ default:
+ return "text-[#9bc3ff]";
+ }
+}
+
+function getStatusBgColor(status: TaskStatus): string {
+ switch (status) {
+ case "pending":
+ return "bg-[rgba(117,170,252,0.1)]";
+ case "initializing":
+ case "starting":
+ return "bg-cyan-400/10";
+ case "running":
+ return "bg-green-400/10";
+ case "paused":
+ return "bg-yellow-400/10";
+ case "blocked":
+ return "bg-orange-400/10";
+ case "done":
+ return "bg-emerald-400/10";
+ case "failed":
+ return "bg-red-400/10";
+ case "merged":
+ return "bg-purple-400/10";
+ default:
+ return "bg-[rgba(117,170,252,0.1)]";
+ }
+}
+
+export function TaskDetail({
+ task,
+ loading,
+ onBack,
+ onSave,
+ onDelete,
+ onStart,
+ onStop,
+ onRestart,
+ onContinue,
+ onSelectSubtask,
+ onCreateSubtask,
+ onToggleSubtaskOutput,
+ viewingSubtaskId,
+ overlayDiff,
+ changedFiles,
+ onRequestDiff,
+ onCreatePR,
+ onAutoMerge,
+ fetchSubtasks,
+}: TaskDetailProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editName, setEditName] = useState(task.name);
+ const [editDescription, setEditDescription] = useState(task.description || "");
+ const [editPlan, setEditPlan] = useState(task.plan);
+ const [editTargetRepoPath, setEditTargetRepoPath] = useState(task.targetRepoPath || "");
+ const [editCompletionAction, setEditCompletionAction] = useState<CompletionAction>(
+ (task.completionAction as CompletionAction) || "none"
+ );
+ const [showDiff, setShowDiff] = useState(false);
+ const [showPRPreview, setShowPRPreview] = useState(false);
+ const [useTreeView, setUseTreeView] = useState(false);
+ // Track which subtask is expanded for inline editing
+ const [expandedSubtaskId, setExpandedSubtaskId] = useState<string | null>(null);
+ // Track interrupt dropdown state
+ const [showInterruptMenu, setShowInterruptMenu] = useState(false);
+ // Track retry completion action state
+ const [isRetryingCompletion, setIsRetryingCompletion] = useState(false);
+ const [retryError, setRetryError] = useState<string | null>(null);
+ // Suggested directories from daemon
+ const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]);
+ // Track clone worktree state
+ const [isCloning, setIsCloning] = useState(false);
+ const [cloneError, setCloneError] = useState<string | null>(null);
+ const [cloneTargetDir, setCloneTargetDir] = useState("");
+
+ // Check if task is running
+ const isTaskRunning = task.status === "running" || task.status === "initializing" || task.status === "starting";
+ // Check if task is in a terminal state (can be continued/reopened)
+ const isTaskTerminal = task.status === "done" || task.status === "failed" || task.status === "merged";
+
+ // Calculate subtask statistics
+ const subtaskStats = useMemo(
+ () => calculateTreeStats(task.subtasks),
+ [task.subtasks]
+ );
+
+ // Check if task can create PR
+ const canCreatePR = useMemo(() => {
+ return (
+ (task.status === "done" || task.status === "merged") &&
+ task.repositoryUrl &&
+ (onCreatePR || onAutoMerge)
+ );
+ }, [task.status, task.repositoryUrl, onCreatePR, onAutoMerge]);
+
+ // Check if task can retry completion action
+ const canRetryCompletion = useMemo(() => {
+ return (
+ (task.status === "done" || task.status === "failed" || task.status === "merged") &&
+ task.completionAction &&
+ task.completionAction !== "none" &&
+ task.targetRepoPath
+ // Note: overlayPath may be null in server DB even if worktree exists on daemon
+ // The daemon will scan for the worktree by task ID
+ );
+ }, [task.status, task.completionAction, task.targetRepoPath]);
+
+ // Handler for retrying completion action
+ const handleRetryCompletion = useCallback(async () => {
+ setIsRetryingCompletion(true);
+ setRetryError(null);
+ try {
+ await retryCompletionAction(task.id);
+ // Success - the result will be shown in task output
+ } catch (e) {
+ setRetryError(e instanceof Error ? e.message : "Failed to retry completion action");
+ } finally {
+ setIsRetryingCompletion(false);
+ }
+ }, [task.id]);
+
+ // Check if task can clone worktree
+ const canCloneWorktree = useMemo(() => {
+ return (
+ (task.status === "done" || task.status === "failed" || task.status === "merged")
+ );
+ }, [task.status]);
+
+ // Handler for cloning worktree
+ const handleCloneWorktree = useCallback(async () => {
+ if (!cloneTargetDir.trim()) {
+ setCloneError("Please enter a target directory");
+ return;
+ }
+ setIsCloning(true);
+ setCloneError(null);
+ try {
+ await cloneWorktree(task.id, cloneTargetDir);
+ // Success - the result will be shown in task output
+ setCloneTargetDir(""); // Clear input on success
+ } catch (e) {
+ setCloneError(e instanceof Error ? e.message : "Failed to clone worktree");
+ } finally {
+ setIsCloning(false);
+ }
+ }, [task.id, cloneTargetDir]);
+
+ // Fetch suggested directories when entering edit mode or when clone section is visible
+ useEffect(() => {
+ if (isEditing || canCloneWorktree) {
+ getDaemonDirectories()
+ .then((res) => setSuggestedDirectories(res.directories))
+ .catch(() => setSuggestedDirectories([]));
+ }
+ }, [isEditing, canCloneWorktree]);
+
+ const handleSave = useCallback(() => {
+ onSave(
+ task.id,
+ editName,
+ editDescription,
+ editPlan,
+ editTargetRepoPath || undefined,
+ editCompletionAction
+ );
+ setIsEditing(false);
+ }, [task.id, editName, editDescription, editPlan, editTargetRepoPath, editCompletionAction, onSave]);
+
+ const handleCancel = useCallback(() => {
+ setEditName(task.name);
+ setEditDescription(task.description || "");
+ setEditPlan(task.plan);
+ setEditTargetRepoPath(task.targetRepoPath || "");
+ setEditCompletionAction((task.completionAction as CompletionAction) || "none");
+ setIsEditing(false);
+ }, [task]);
+
+ // Toggle subtask expansion for inline editing
+ const handleSubtaskToggle = useCallback((subtaskId: string) => {
+ setExpandedSubtaskId((prev) => (prev === subtaskId ? null : subtaskId));
+ }, []);
+
+ // Handle subtask click - toggle output view for any task status
+ const handleSubtaskClick = useCallback(
+ (subtask: TaskSummary) => {
+ if (onToggleSubtaskOutput) {
+ // Toggle viewing this subtask's output (works for any status)
+ onToggleSubtaskOutput(subtask.id, subtask.name);
+ } else {
+ // Fallback to expand/collapse if output viewing not available
+ handleSubtaskToggle(subtask.id);
+ }
+ },
+ [onToggleSubtaskOutput, handleSubtaskToggle]
+ );
+
+ // Called when inline subtask editor saves changes
+ const handleSubtaskUpdated = useCallback(() => {
+ // Re-fetch the parent task to refresh subtask list
+ // This will trigger from the parent component when task updates
+ }, []);
+
+ if (loading) {
+ return (
+ <div className="panel h-full flex items-center justify-center">
+ <div className="font-mono text-[#9bc3ff] text-sm">Loading task...</div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="panel h-full flex flex-col overflow-hidden">
+ {/* Header */}
+ <div className="flex items-center justify-between p-4 pb-2 border-b border-dashed border-[rgba(117,170,252,0.35)] shrink-0">
+ <div className="flex items-center gap-3">
+ <button
+ onClick={onBack}
+ className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
+ >
+ &lt; Back
+ </button>
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ TASK//
+ </div>
+ </div>
+ <div className="flex items-center gap-2">
+ {isEditing ? (
+ <>
+ <button
+ onClick={handleCancel}
+ className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleSave}
+ className="px-3 py-1 font-mono text-xs text-green-400 border border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10 transition-colors uppercase"
+ >
+ Save
+ </button>
+ </>
+ ) : (
+ <>
+ {(task.status === "pending" || task.status === "failed") && (
+ <button
+ onClick={() => onStart(task.id)}
+ className="px-3 py-1 font-mono text-xs text-green-400 border border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10 transition-colors uppercase"
+ >
+ Start
+ </button>
+ )}
+ {isTaskRunning && (
+ <div className="relative">
+ <button
+ onClick={() => setShowInterruptMenu(!showInterruptMenu)}
+ className="px-3 py-1 font-mono text-xs text-orange-400 border border-orange-400/30 hover:border-orange-400/50 hover:bg-orange-400/10 transition-colors uppercase flex items-center gap-1"
+ >
+ <span className="w-1.5 h-1.5 bg-orange-400 rounded-full animate-pulse" />
+ Interrupt
+ </button>
+ {showInterruptMenu && (
+ <>
+ {/* Backdrop to close menu on click outside */}
+ <div
+ className="fixed inset-0 z-40"
+ onClick={() => setShowInterruptMenu(false)}
+ />
+ <div className="absolute right-0 top-full mt-1 z-50 bg-[#0a1525] border border-[rgba(117,170,252,0.35)] shadow-lg">
+ <button
+ onClick={() => {
+ onRestart(task.id);
+ setShowInterruptMenu(false);
+ }}
+ className="block w-full px-4 py-2 font-mono text-xs text-left text-yellow-400 hover:bg-yellow-400/10 transition-colors whitespace-nowrap"
+ >
+ Restart Task
+ </button>
+ <button
+ onClick={() => {
+ onStop(task.id);
+ setShowInterruptMenu(false);
+ }}
+ className="block w-full px-4 py-2 font-mono text-xs text-left text-red-400 hover:bg-red-400/10 transition-colors whitespace-nowrap"
+ >
+ Cancel Task
+ </button>
+ </div>
+ </>
+ )}
+ </div>
+ )}
+ {isTaskTerminal && (
+ <button
+ onClick={() => onContinue(task.id)}
+ className="px-3 py-1 font-mono text-xs text-cyan-400 border border-cyan-400/30 hover:border-cyan-400/50 hover:bg-cyan-400/10 transition-colors uppercase flex items-center gap-1"
+ >
+ <span className="w-1.5 h-1.5 bg-cyan-400 rounded-full" />
+ Continue
+ </button>
+ )}
+ <button
+ onClick={() => setIsEditing(true)}
+ className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ Edit
+ </button>
+ <button
+ onClick={() => onDelete(task.id)}
+ className="px-3 py-1 font-mono text-xs text-red-400 border border-red-400/30 hover:border-red-400/50 transition-colors uppercase"
+ >
+ Delete
+ </button>
+ </>
+ )}
+ </div>
+ </div>
+
+ {/* Content */}
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
+ {/* Task Info */}
+ <div className="space-y-3">
+ {isEditing ? (
+ <>
+ <input
+ type="text"
+ value={editName}
+ onChange={(e) => setEditName(e.target.value)}
+ className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-lg px-3 py-2 outline-none focus:border-[#3f6fb3]"
+ placeholder="Task name"
+ />
+ <textarea
+ value={editDescription}
+ onChange={(e) => setEditDescription(e.target.value)}
+ className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3] min-h-[60px] resize-y"
+ placeholder="Description (optional)"
+ />
+ </>
+ ) : (
+ <>
+ <h2 className="font-mono text-lg text-[#dbe7ff]">{task.name}</h2>
+ {task.description && (
+ <p className="font-mono text-sm text-[#9bc3ff]">{task.description}</p>
+ )}
+ </>
+ )}
+
+ {/* Status badges */}
+ <div className="flex flex-wrap gap-2">
+ <span
+ className={`px-2 py-0.5 font-mono text-xs uppercase ${getStatusColor(
+ task.status as TaskStatus
+ )} ${getStatusBgColor(task.status as TaskStatus)} border border-current/20`}
+ >
+ {task.status}
+ </span>
+ {/* Orchestrator badge for depth 0 tasks with subtasks */}
+ {task.depth === 0 && task.subtasks.length > 0 && (
+ <span className="px-2 py-0.5 font-mono text-xs text-purple-400 bg-purple-400/10 border border-purple-400/20">
+ Orchestrator
+ </span>
+ )}
+ {/* Depth indicator for subtasks */}
+ {task.depth > 0 && (
+ <span className="px-2 py-0.5 font-mono text-xs text-cyan-400 bg-cyan-400/10 border border-cyan-400/20">
+ Depth: {task.depth}
+ </span>
+ )}
+ {task.priority > 0 && (
+ <span className="px-2 py-0.5 font-mono text-xs text-orange-400 bg-orange-400/10 border border-orange-400/20">
+ Priority: {task.priority}
+ </span>
+ )}
+ {task.mergeMode && (
+ <span className="px-2 py-0.5 font-mono text-xs text-purple-400 bg-purple-400/10 border border-purple-400/20">
+ Merge: {task.mergeMode}
+ </span>
+ )}
+ </div>
+
+ {/* Metadata */}
+ <div className="flex flex-wrap gap-4 font-mono text-[10px] text-[#75aafc]">
+ <span>Created: {formatDate(task.createdAt)}</span>
+ {task.startedAt && <span>Started: {formatDate(task.startedAt)}</span>}
+ {task.completedAt && <span>Completed: {formatDate(task.completedAt)}</span>}
+ <span>Version: {task.version}</span>
+ </div>
+ </div>
+
+ {/* Plan */}
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Plan
+ </div>
+ {isEditing ? (
+ <textarea
+ value={editPlan}
+ onChange={(e) => setEditPlan(e.target.value)}
+ className="w-full bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3] min-h-[200px] resize-y"
+ placeholder="Enter the plan/instructions for this task..."
+ />
+ ) : (
+ <pre className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap overflow-x-auto">
+ {task.plan}
+ </pre>
+ )}
+ </div>
+
+ {/* Progress Summary */}
+ {task.progressSummary && (
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Progress
+ </div>
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 font-mono text-sm text-[#9bc3ff]">
+ {task.progressSummary}
+ </div>
+ </div>
+ )}
+
+ {/* Last Output */}
+ {task.lastOutput && (
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Last Output
+ </div>
+ <pre className="bg-[rgba(0,0,0,0.3)] border border-[rgba(117,170,252,0.15)] p-3 font-mono text-xs text-[#75aafc] whitespace-pre-wrap overflow-x-auto max-h-[200px] overflow-y-auto">
+ {task.lastOutput}
+ </pre>
+ </div>
+ )}
+
+ {/* Error Message */}
+ {task.errorMessage && (
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-red-400 tracking-wide uppercase">
+ Error
+ </div>
+ <div className="bg-red-400/5 border border-red-400/30 p-3 font-mono text-sm text-red-400">
+ {task.errorMessage}
+ </div>
+ </div>
+ )}
+
+ {/* Repository Info */}
+ {(task.repositoryUrl || task.baseBranch || task.targetBranch) && (
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Repository
+ </div>
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-1">
+ {task.repositoryUrl && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">URL:</span> {task.repositoryUrl}
+ </div>
+ )}
+ {task.baseBranch && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Base:</span> {task.baseBranch}
+ </div>
+ )}
+ {task.targetBranch && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Target:</span> {task.targetBranch}
+ </div>
+ )}
+ {task.prUrl && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">PR:</span>{" "}
+ <a
+ href={task.prUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="text-[#9bc3ff] hover:underline"
+ >
+ {task.prUrl}
+ </a>
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+
+ {/* Completion Action Settings */}
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Completion Actions
+ </div>
+ {isEditing ? (
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-3">
+ <div className="space-y-1">
+ <label className="font-mono text-xs text-[#555]">Action on Completion</label>
+ <select
+ value={editCompletionAction}
+ onChange={(e) => setEditCompletionAction(e.target.value as CompletionAction)}
+ className="w-full bg-[#0a1525] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-3 py-2 outline-none focus:border-[#3f6fb3]"
+ >
+ <option value="none">None (keep in worktree)</option>
+ <option value="branch">Create branch in target repo</option>
+ <option value="merge">Auto-merge to target branch</option>
+ <option value="pr">Create Pull Request</option>
+ </select>
+ </div>
+ {editCompletionAction !== "none" && (
+ <div className="space-y-1">
+ <label className="font-mono text-xs text-[#555]">Target Repository Path</label>
+ <DirectoryInput
+ value={editTargetRepoPath}
+ onChange={setEditTargetRepoPath}
+ suggestions={suggestedDirectories}
+ placeholder="/path/to/your/local/repo"
+ repoUrl={task.repositoryUrl}
+ />
+ <p className="font-mono text-[10px] text-[#555]">
+ Path to your local repository where the branch will be pushed/merged.
+ </p>
+ </div>
+ )}
+ </div>
+ ) : (
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-1">
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Action:</span>{" "}
+ {task.completionAction === "none" || !task.completionAction
+ ? "None (keep in worktree)"
+ : task.completionAction === "branch"
+ ? "Create branch in target repo"
+ : task.completionAction === "merge"
+ ? "Auto-merge to target branch"
+ : task.completionAction === "pr"
+ ? "Create Pull Request"
+ : task.completionAction}
+ </div>
+ {task.targetRepoPath && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Target Repo:</span> {task.targetRepoPath}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* Metadata Info */}
+ {(task.daemonId || task.containerId || task.overlayPath) && (
+ <div className="space-y-2">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Metadata
+ </div>
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-1">
+ {task.daemonId && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Daemon:</span> {task.daemonId}
+ </div>
+ )}
+ {task.containerId && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Container:</span> {task.containerId}
+ </div>
+ )}
+ {task.overlayPath && (
+ <div className="font-mono text-xs text-[#75aafc]">
+ <span className="text-[#555]">Overlay:</span> {task.overlayPath}
+ </div>
+ )}
+ </div>
+ </div>
+ )}
+
+ {/* Subtasks */}
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-3">
+ <div className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
+ Subtasks ({task.subtasks.length})
+ </div>
+ {task.subtasks.length > 0 && (
+ <button
+ onClick={() => setUseTreeView(!useTreeView)}
+ className="font-mono text-[9px] text-[#555] hover:text-[#75aafc]"
+ >
+ {useTreeView ? "List" : "Tree"}
+ </button>
+ )}
+ </div>
+ {/* Disable adding subtasks at max depth (2 = sub-subtask, cannot have children) */}
+ {task.depth < 2 ? (
+ <button
+ onClick={onCreateSubtask}
+ className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
+ >
+ + Add Subtask
+ </button>
+ ) : (
+ <span className="px-2 py-1 font-mono text-[10px] text-[#555] border border-[#333]" title="Maximum depth reached">
+ Max depth
+ </span>
+ )}
+ </div>
+
+ {/* Progress bar for subtasks */}
+ {task.subtasks.length > 0 && (
+ <SubtaskProgressBar stats={subtaskStats} />
+ )}
+
+ {task.subtasks.length === 0 ? (
+ <div className="text-[#555] font-mono text-xs py-4 text-center">
+ No subtasks yet
+ </div>
+ ) : useTreeView ? (
+ <div className="border border-[rgba(117,170,252,0.15)]">
+ <SubtaskTree
+ subtasks={task.subtasks}
+ onSelect={onSelectSubtask}
+ fetchSubtasks={fetchSubtasks}
+ />
+ </div>
+ ) : (
+ <div className="divide-y divide-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.15)]">
+ {task.subtasks.map((subtask: TaskSummary) => {
+ const isRunning = subtask.status === "running" || subtask.status === "initializing" || subtask.status === "starting";
+ const isViewingOutput = viewingSubtaskId === subtask.id;
+ const isExpanded = expandedSubtaskId === subtask.id;
+
+ // Different highlight colors: green for running, subtle blue for others
+ const outputHighlightBg = isRunning ? "bg-green-400/10" : "bg-[rgba(117,170,252,0.08)]";
+ const outputHighlightBorder = isRunning ? "border-l-green-400" : "border-l-[#75aafc]";
+ const outputLabelColor = isRunning ? "text-green-400" : "text-[#75aafc]";
+
+ return (
+ <div key={subtask.id}>
+ {/* Subtask header - clickable to view output */}
+ <div
+ className={`w-full p-3 text-left hover:bg-[rgba(117,170,252,0.05)] transition-colors cursor-pointer ${
+ isExpanded && !isViewingOutput ? "bg-[rgba(117,170,252,0.08)]" : ""
+ } ${isViewingOutput ? `${outputHighlightBg} border-l-2 ${outputHighlightBorder}` : ""}`}
+ onClick={() => handleSubtaskClick(subtask)}
+ >
+ <div className="flex items-center gap-2 mb-1">
+ <span className="text-[#555] text-xs">
+ {isViewingOutput ? "[*]" : (isExpanded ? "[-]" : "[+]")}
+ </span>
+ <span className="font-mono text-sm text-[#dbe7ff]">
+ {subtask.name}
+ </span>
+ <span
+ className={`px-1.5 py-0.5 font-mono text-[9px] uppercase ${getStatusColor(
+ subtask.status
+ )} ${getStatusBgColor(subtask.status)} border border-current/20`}
+ >
+ {subtask.status}
+ </span>
+ {subtask.subtaskCount > 0 && (
+ <span className="font-mono text-[9px] text-[#555]">
+ +{subtask.subtaskCount}
+ </span>
+ )}
+ {isViewingOutput && (
+ <span className={`font-mono text-[9px] ${outputLabelColor} ml-auto`}>
+ {isRunning ? "viewing live output" : "viewing output"}
+ </span>
+ )}
+ {/* Expand/edit button - always available */}
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ handleSubtaskToggle(subtask.id);
+ }}
+ className={`ml-auto px-1.5 py-0.5 font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors ${
+ isViewingOutput ? "ml-2" : ""
+ }`}
+ title="Expand details"
+ >
+ {isExpanded ? "-" : "+"}
+ </button>
+ </div>
+ {subtask.progressSummary && !isExpanded && !isViewingOutput && (
+ <p className="font-mono text-xs text-[#75aafc] line-clamp-1 pl-6">
+ {subtask.progressSummary}
+ </p>
+ )}
+ </div>
+ {/* Inline subtask editor */}
+ {isExpanded && (
+ <InlineSubtaskEditor
+ subtaskId={subtask.id}
+ onClose={() => setExpandedSubtaskId(null)}
+ onUpdated={handleSubtaskUpdated}
+ onNavigate={onSelectSubtask}
+ />
+ )}
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </div>
+
+ {/* Action buttons for completed tasks */}
+ {(task.status === "done" || task.status === "merged" || task.status === "failed") && (
+ <div className="space-y-2 pt-4 border-t border-[rgba(117,170,252,0.2)]">
+ <div className="flex flex-wrap gap-2">
+ {onRequestDiff && (
+ <button
+ onClick={() => {
+ onRequestDiff();
+ setShowDiff(true);
+ }}
+ className="px-3 py-1.5 font-mono text-xs text-[#75aafc] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors"
+ >
+ View Diff
+ </button>
+ )}
+ {canCreatePR && (
+ <button
+ onClick={() => setShowPRPreview(true)}
+ className="px-3 py-1.5 font-mono text-xs text-green-400 border border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10 transition-colors"
+ >
+ Create PR
+ </button>
+ )}
+ {/* Retry completion action button */}
+ {canRetryCompletion && (
+ <button
+ onClick={handleRetryCompletion}
+ disabled={isRetryingCompletion}
+ className="px-3 py-1.5 font-mono text-xs text-cyan-400 border border-cyan-400/30 hover:border-cyan-400/50 hover:bg-cyan-400/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {isRetryingCompletion
+ ? "Retrying..."
+ : task.completionAction === "branch"
+ ? "Push Branch"
+ : task.completionAction === "merge"
+ ? "Merge to Target"
+ : task.completionAction === "pr"
+ ? "Create PR"
+ : "Run Completion Action"}
+ </button>
+ )}
+ {/* Show hint if completion action needs configuration */}
+ {!canRetryCompletion && (
+ <span className="px-3 py-1.5 font-mono text-xs text-[#555] italic">
+ {!task.completionAction || task.completionAction === "none"
+ ? "Set completion action to enable"
+ : !task.targetRepoPath
+ ? "Set target repo path to enable"
+ : ""}
+ </span>
+ )}
+ </div>
+ {/* Retry error message */}
+ {retryError && (
+ <div className="font-mono text-xs text-red-400 bg-red-400/10 px-3 py-2 border border-red-400/30">
+ {retryError}
+ </div>
+ )}
+ </div>
+ )}
+
+ {/* Clone Worktree Section */}
+ {canCloneWorktree && (
+ <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-3 space-y-2">
+ <div className="font-mono text-xs text-[#555]">Clone Worktree to Directory</div>
+ <div className="flex gap-2 items-start">
+ <DirectoryInput
+ value={cloneTargetDir}
+ onChange={setCloneTargetDir}
+ suggestions={suggestedDirectories}
+ placeholder="/path/to/clone"
+ repoUrl={task.repositoryUrl}
+ className="flex-1"
+ />
+ <button
+ onClick={handleCloneWorktree}
+ disabled={isCloning || !cloneTargetDir.trim()}
+ className="px-3 py-2 font-mono text-xs text-purple-400 border border-purple-400/30 hover:border-purple-400/50 hover:bg-purple-400/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
+ >
+ {isCloning ? "Cloning..." : "Clone"}
+ </button>
+ </div>
+ <p className="font-mono text-[10px] text-[#555]">
+ Clone the worktree (git repo) to a new directory. Useful for moving completed work outside ~/.makima.
+ </p>
+ {cloneError && (
+ <div className="font-mono text-xs text-red-400 bg-red-400/10 px-3 py-2 border border-red-400/30">
+ {cloneError}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+
+ {/* Overlay Diff Modal */}
+ {showDiff && overlayDiff !== undefined && (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
+ <div className="max-w-4xl w-full max-h-[80vh]">
+ <OverlayDiffViewer
+ diff={overlayDiff}
+ changedFiles={changedFiles}
+ onClose={() => setShowDiff(false)}
+ title={`Changes in ${task.name}`}
+ />
+ </div>
+ </div>
+ )}
+
+ {/* PR Preview Modal */}
+ {showPRPreview && (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
+ <div className="max-w-3xl w-full">
+ <PRPreview
+ task={task}
+ diff={overlayDiff}
+ changedFiles={changedFiles}
+ onCreatePR={onCreatePR}
+ onAutoMerge={task.mergeMode === "auto" ? onAutoMerge : undefined}
+ onClose={() => setShowPRPreview(false)}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}