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";
import { BranchTaskModal } from "./BranchTaskModal";
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;
/** Navigate to view the contract */
onViewContract?: (contractId: string) => void;
/** Branch the task to create a new task with same state */
onBranch?: (taskId: string, message: string, name?: string) => Promise<void>;
// 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[]>;
/** For supervisor tasks: all tasks in the contract (excluding the supervisor itself) */
contractTasks?: 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,
onViewContract,
onBranch,
overlayDiff,
changedFiles,
onRequestDiff,
onCreatePR,
onAutoMerge,
fetchSubtasks,
contractTasks,
}: 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("");
// Track branch modal state
const [showBranchModal, setShowBranchModal] = useState(false);
// 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";
// Check if this is a supervisor task
const isSupervisor = task.isSupervisor === true;
// Show continue for supervisors (always) or terminal states for other tasks
const canContinue = isSupervisor || isTaskTerminal;
// Show branch button when task has run at least once (not pending)
const canBranch = onBranch && task.status !== "pending";
// Determine which tasks to show: for supervisors, show contractTasks; for regular tasks, show subtasks
const displayTasks = useMemo(() => {
if (isSupervisor && contractTasks) {
return contractTasks;
}
return task.subtasks;
}, [isSupervisor, contractTasks, task.subtasks]);
// Calculate task statistics for progress bar
const subtaskStats = useMemo(
() => calculateTreeStats(displayTasks),
[displayTasks]
);
// 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"
>
< 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>
)}
{canContinue && (
<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>
)}
{canBranch && (
<button
onClick={() => setShowBranchModal(true)}
className="px-3 py-1 font-mono text-xs text-purple-400 border border-purple-400/30 hover:border-purple-400/50 hover:bg-purple-400/10 transition-colors uppercase flex items-center gap-1"
>
<span className="w-1.5 h-1.5 bg-purple-400 rounded-full" />
Branch
</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>
{/* Contract badge - clickable to view contract */}
{task.contractId && onViewContract && (
<button
onClick={() => onViewContract(task.contractId!)}
className="px-2 py-0.5 font-mono text-xs text-blue-400 bg-blue-400/10 border border-blue-400/20 hover:bg-blue-400/20 transition-colors"
>
Contract
</button>
)}
{/* 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 / Contract Tasks (for supervisors) */}
<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">
{isSupervisor ? `Contract Tasks (${displayTasks.length})` : `Subtasks (${displayTasks.length})`}
</div>
{displayTasks.length > 0 && (
<button
onClick={() => setUseTreeView(!useTreeView)}
className="font-mono text-[9px] text-[#555] hover:text-[#75aafc]"
>
{useTreeView ? "List" : "Tree"}
</button>
)}
</div>
{/* Disable adding subtasks for supervisors and at max depth (2 = sub-subtask, cannot have children) */}
{!isSupervisor && 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>
) : !isSupervisor ? (
<span className="px-2 py-1 font-mono text-[10px] text-[#555] border border-[#333]" title="Maximum depth reached">
Max depth
</span>
) : null}
</div>
{/* Progress bar for tasks */}
{displayTasks.length > 0 && (
<SubtaskProgressBar stats={subtaskStats} />
)}
{displayTasks.length === 0 ? (
<div className="text-[#555] font-mono text-xs py-4 text-center">
{isSupervisor ? "No tasks in contract yet" : "No subtasks yet"}
</div>
) : useTreeView ? (
<div className="border border-[rgba(117,170,252,0.15)]">
<SubtaskTree
subtasks={displayTasks}
onSelect={onSelectSubtask}
fetchSubtasks={fetchSubtasks}
/>
</div>
) : (
<div className="divide-y divide-[rgba(117,170,252,0.15)] border border-[rgba(117,170,252,0.15)]">
{displayTasks.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>
)}
{/* Branch Task Modal */}
{showBranchModal && onBranch && (
<BranchTaskModal
task={task}
onBranch={onBranch}
onClose={() => setShowBranchModal(false)}
/>
)}
</div>
);
}