import { useState, useCallback, useEffect, useRef, useMemo, type MouseEvent } from "react"; import { useParams, useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { TaskList } from "../components/mesh/TaskList"; import { TaskDetail } from "../components/mesh/TaskDetail"; import { TaskOutput } from "../components/mesh/TaskOutput"; import { ContractCompleteQuestion } from "../components/mesh/ContractCompleteQuestion"; import { useTasks } from "../hooks/useTasks"; import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription"; import type { TaskWithSubtasks, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi, resumeSupervisor, branchTask, getRepositorySuggestions, getTaskDiff } from "../lib/api"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useAuth } from "../contexts/AuthContext"; import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; // View modes for the task detail page type ViewMode = "split" | "task" | "output"; // Minimum panel widths (in pixels) const MIN_TASK_WIDTH = 300; const MIN_OUTPUT_WIDTH = 200; // TODO: Store task output in database for resuming from any device. // Currently only persisted in localStorage which is device-specific. // LocalStorage key prefix for task output const STORAGE_KEY_PREFIX_OUTPUT = "makima-task-output-"; // Load persisted output from localStorage with deduplication function loadPersistedOutput(taskId: string): TaskOutputEvent[] { try { const stored = localStorage.getItem(STORAGE_KEY_PREFIX_OUTPUT + taskId); if (!stored) return []; const entries = JSON.parse(stored) as TaskOutputEvent[]; // Deduplicate consecutive identical entries (cleanup from previous bug) const deduplicated: TaskOutputEvent[] = []; for (const entry of entries) { const last = deduplicated[deduplicated.length - 1]; if ( !last || last.messageType !== entry.messageType || last.content !== entry.content || last.toolName !== entry.toolName ) { deduplicated.push(entry); } } // Save cleaned up version if we removed duplicates if (deduplicated.length !== entries.length) { savePersistedOutput(taskId, deduplicated); } return deduplicated; } catch { return []; } } // Save output to localStorage function savePersistedOutput(taskId: string, entries: TaskOutputEvent[]): void { try { localStorage.setItem(STORAGE_KEY_PREFIX_OUTPUT + taskId, JSON.stringify(entries)); } catch { // Ignore storage errors } } // Clear output from localStorage function clearPersistedOutput(taskId: string): void { try { localStorage.removeItem(STORAGE_KEY_PREFIX_OUTPUT + taskId); } catch { // Ignore storage errors } } export default function MeshPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); const { tasks, loading, error, conflict, clearConflict, fetchTask, fetchTasks, editTask, removeTask, hideTask, saveTask } = useTasks(); const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); // Memoize pending question IDs for efficient lookup const pendingQuestionIds = useMemo( () => new Set(pendingQuestions.map(q => q.questionId)), [pendingQuestions] ); // Filter contract_complete questions for the current task const contractCompleteQuestionsForTask = useMemo( () => pendingQuestions.filter( (q) => q.questionType === "contract_complete" && q.taskId === id ), [pendingQuestions, id] ); // Handler for answering supervisor questions const handleAnswerQuestion = useCallback(async (questionId: string, response: string) => { await submitAnswer(questionId, response); }, [submitAnswer]); // Redirect to login if not authenticated useEffect(() => { if (!authLoading && isAuthConfigured && !isAuthenticated) { navigate("/login"); } }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); const [taskDetail, setTaskDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [creating, setCreating] = useState(false); const [taskOutputEntries, setTaskOutputEntries] = useState([]); const [isStreaming, setIsStreaming] = useState(false); // Contract selection modal state const [showContractModal, setShowContractModal] = useState(false); const [contracts, setContracts] = useState([]); const [contractsLoading, setContractsLoading] = useState(false); // Task creation modal (step 2) const [modalStep, setModalStep] = useState<1 | 2>(1); const [selectedContract, setSelectedContract] = useState(null); const [daemonDirectories, setDaemonDirectories] = useState([]); const [newTaskName, setNewTaskName] = useState(""); const [newTaskRepoUrl, setNewTaskRepoUrl] = useState(null); const [newTaskTargetPath, setNewTaskTargetPath] = useState(""); // Standalone task repository selection state const [standaloneRepoType, setStandaloneRepoType] = useState<"remote" | "local">("remote"); const [standaloneRepoUrl, setStandaloneRepoUrl] = useState(""); const [standaloneRepoPath, setStandaloneRepoPath] = useState(""); const [repoSuggestions, setRepoSuggestions] = useState([]); const [showRepoSuggestions, setShowRepoSuggestions] = useState(false); // Track which subtask's output we're viewing (null = parent task) const [viewingSubtaskId, setViewingSubtaskId] = useState(null); const [viewingSubtaskName, setViewingSubtaskName] = useState(null); // For supervisor tasks: all tasks in the contract (excluding the supervisor itself) const [contractTasks, setContractTasks] = useState([]); // Overlay diff content for viewing worktree changes const [overlayDiff, setOverlayDiff] = useState(undefined); // View mode for the split panel layout const [viewMode, setViewMode] = useState("split"); // Width of the task panel as a percentage (0-100) const [taskPanelPercent, setTaskPanelPercent] = useState(66.67); // Track resizing state const [isResizing, setIsResizing] = useState(false); const containerRef = useRef(null); // Track which task we've loaded output for to avoid stale saves const loadedTaskIdRef = useRef(null); // Handle task update events from WebSocket const handleTaskUpdate = useCallback(async (event: TaskUpdateEvent) => { // Always refresh task list when a new task is created (e.g., by supervisor) // This ensures newly spawned tasks appear in the sidebar immediately const isNewTask = event.updatedFields.includes("created"); if (isNewTask) { fetchTasks(); } // Refresh task list if we're viewing the list (no specific task selected) if (!id) { if (!isNewTask) { // Only fetch if we didn't already fetch for new task creation fetchTasks(); } return; } // Check if this update is for the current task or one of its subtasks const isCurrentTask = event.taskId === id; const isSubtask = taskDetail?.subtasks.some((st) => st.id === event.taskId); // Refresh task detail if the update is for current task or any subtask // This ensures subtask status changes (e.g., when orchestrator starts them) are reflected if (isCurrentTask || isSubtask) { const updated = await fetchTask(id); if (updated) { setTaskDetail(updated); } } // Update streaming state based on status for current task if (isCurrentTask) { setIsStreaming(event.status === "running"); } }, [id, fetchTask, fetchTasks, taskDetail?.subtasks]); // The task ID whose output we're currently viewing const activeOutputTaskId = viewingSubtaskId || id; // Handle task output events from WebSocket const handleTaskOutput = useCallback((event: TaskOutputEvent) => { // Only process output for the task we're currently viewing if (event.taskId === activeOutputTaskId) { setTaskOutputEntries((prev) => { // Deduplicate by checking if last entry is identical // This prevents duplicates from React StrictMode or WebSocket reconnects const lastEntry = prev[prev.length - 1]; if ( lastEntry && lastEntry.messageType === event.messageType && lastEntry.content === event.content && lastEntry.toolName === event.toolName ) { return prev; // Skip duplicate } const newEntries = [...prev, event]; // Persist to localStorage savePersistedOutput(event.taskId, newEntries); return newEntries; }); } }, [activeOutputTaskId]); // Handle user input sent to task - show immediately in output const handleUserInput = useCallback((message: string) => { if (!activeOutputTaskId) return; const userEntry: TaskOutputEvent = { taskId: activeOutputTaskId, messageType: "user_input", content: message, isPartial: false, }; setTaskOutputEntries((prev) => { const newEntries = [...prev, userEntry]; savePersistedOutput(activeOutputTaskId, newEntries); return newEntries; }); }, [activeOutputTaskId]); // Subscribe to task updates and output // When viewing a subtask's output, subscribe to that instead of the parent // Always subscribe to all updates so we see subtask status changes const { connected } = useTaskSubscription({ taskId: id || null, subscribeAll: true, // Always subscribe to all - needed to see subtask updates subscribeOutput: !!activeOutputTaskId, // Subscribe to output when viewing a task outputTaskId: activeOutputTaskId || undefined, // Which task's output to subscribe to onUpdate: handleTaskUpdate, onOutput: handleTaskOutput, }); // Load persisted output when task or viewed subtask changes useEffect(() => { if (activeOutputTaskId) { // First load from localStorage (instant, for local cache) const persisted = loadPersistedOutput(activeOutputTaskId); setTaskOutputEntries(persisted); loadedTaskIdRef.current = activeOutputTaskId; // Then fetch from API to get any output we missed // (e.g., subtask was running before we started viewing it) getTaskOutput(activeOutputTaskId) .then((response) => { if (response.entries.length > 0) { setTaskOutputEntries((prev) => { // API returns all historical entries in chronological order const apiEntries = response.entries.map(entry => ({ taskId: entry.taskId, messageType: entry.messageType, content: entry.content, toolName: entry.toolName, toolInput: entry.toolInput, isError: entry.isError, costUsd: entry.costUsd, durationMs: entry.durationMs, isPartial: false, })); // If localStorage is empty, just use API data if (prev.length === 0) { savePersistedOutput(activeOutputTaskId, apiEntries); return apiEntries; } // localStorage has user_input entries in correct positions - trust its order // Only append API entries that we don't already have locally const localKeys = new Set(prev.map(e => `${e.messageType}:${e.content}`)); const newFromApi = apiEntries.filter(e => !localKeys.has(`${e.messageType}:${e.content}`)); // Keep local order (has user_input in correct spots), append new API data const merged = [...prev, ...newFromApi]; savePersistedOutput(activeOutputTaskId, merged); return merged; }); } }) .catch((err) => { console.error("Failed to fetch task output:", err); }); } else { setTaskOutputEntries([]); loadedTaskIdRef.current = null; } setIsStreaming(false); }, [activeOutputTaskId]); // Reset subtask view when navigating to a different parent task useEffect(() => { setViewingSubtaskId(null); setViewingSubtaskName(null); setOverlayDiff(undefined); }, [id]); // Toggle viewing a subtask's output (for running subtasks) const handleToggleSubtaskOutput = useCallback( (subtaskId: string, subtaskName: string) => { if (viewingSubtaskId === subtaskId) { // Already viewing this subtask, switch back to parent setViewingSubtaskId(null); setViewingSubtaskName(null); } else { // Switch to viewing this subtask's output setViewingSubtaskId(subtaskId); setViewingSubtaskName(subtaskName); } }, [viewingSubtaskId] ); // Request diff for the current task const handleRequestDiff = useCallback(async () => { if (!id) return; try { const result = await getTaskDiff(id); if (result.success && result.diff) { setOverlayDiff(result.diff); } else { setOverlayDiff(result.error || "Failed to get diff"); } } catch (e) { console.error("Failed to get diff:", e); setOverlayDiff(e instanceof Error ? e.message : "Failed to get diff"); } }, [id]); // Load task detail when URL has an id useEffect(() => { if (id) { setDetailLoading(true); fetchTask(id).then((detail) => { setTaskDetail(detail); setDetailLoading(false); }); } else { setTaskDetail(null); } }, [id, fetchTask]); // For supervisor tasks: fetch all tasks in the contract (excluding the supervisor itself) useEffect(() => { if (taskDetail?.isSupervisor && taskDetail.contractId) { getContract(taskDetail.contractId) .then((contract) => { // Filter out the supervisor task itself const tasksExcludingSupervisor = contract.tasks.filter( (t) => t.id !== taskDetail.id ); setContractTasks(tasksExcludingSupervisor); }) .catch((err) => { console.error("Failed to fetch contract tasks for supervisor:", err); setContractTasks([]); }); } else { setContractTasks([]); } }, [taskDetail?.isSupervisor, taskDetail?.contractId, taskDetail?.id]); // Fetch repository suggestions when standalone task modal is open useEffect(() => { if (showContractModal && modalStep === 2 && !selectedContract) { getRepositorySuggestions(standaloneRepoType, undefined, 10) .then((res) => { setRepoSuggestions(res.entries); setShowRepoSuggestions(res.entries.length > 0); }) .catch(() => { setRepoSuggestions([]); setShowRepoSuggestions(false); }); } else if (!showContractModal) { setRepoSuggestions([]); setShowRepoSuggestions(false); } }, [showContractModal, modalStep, selectedContract, standaloneRepoType]); // Apply a repository suggestion const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => { if (suggestion.repositoryUrl) { setStandaloneRepoUrl(suggestion.repositoryUrl); } if (suggestion.localPath) { setStandaloneRepoPath(suggestion.localPath); } setShowRepoSuggestions(false); }, []); const handleSelectTask = useCallback( (taskId: string) => { navigate(`/exec/${taskId}`); }, [navigate] ); const handleBack = useCallback(() => { // If viewing a subtask, go back to parent if (taskDetail?.parentTaskId) { navigate(`/exec/${taskDetail.parentTaskId}`); } else { navigate("/exec"); } }, [navigate, taskDetail]); const handleDelete = useCallback( async (taskId: string) => { if (confirm("Are you sure you want to delete this task?")) { const success = await removeTask(taskId); if (success && id === taskId) { // If deleting current task, go back if (taskDetail?.parentTaskId) { navigate(`/exec/${taskDetail.parentTaskId}`); } else { navigate("/exec"); } } } }, [removeTask, id, taskDetail, navigate] ); const handleDismiss = useCallback( async (taskId: string) => { await hideTask(taskId); }, [hideTask] ); const handleStart = useCallback( async (taskId: string) => { try { const updated = await startTaskApi(taskId); setTaskDetail((prev) => prev ? { ...prev, ...updated } : prev); } catch (e) { console.error("Failed to start task:", e); alert(e instanceof Error ? e.message : "Failed to start task"); } }, [] ); const handleStop = useCallback( async (taskId: string) => { try { const updated = await stopTaskApi(taskId); setTaskDetail((prev) => prev ? { ...prev, ...updated } : prev); } catch (e) { console.error("Failed to stop task:", e); alert(e instanceof Error ? e.message : "Failed to stop task"); } }, [] ); const handleRestart = useCallback( async (taskId: string) => { try { // First stop the task await stopTaskApi(taskId); // Then start it again const updated = await startTaskApi(taskId); setTaskDetail((prev) => prev ? { ...prev, ...updated } : prev); } catch (e) { console.error("Failed to restart task:", e); alert(e instanceof Error ? e.message : "Failed to restart task"); } }, [] ); const handleContinue = useCallback( async (taskId: string) => { try { // Check if this is a supervisor task - use resumeSupervisor API instead if (taskDetail?.isSupervisor && taskDetail?.contractId) { const result = await resumeSupervisor(taskDetail.contractId, { resumeMode: "continue", }); console.log(`[Mesh] Supervisor resumed, daemon: ${result.daemonId}`); // Refresh task detail to get updated state const updated = await fetchTask(taskId); if (updated) { setTaskDetail(updated); } } else { // Continue regular task with conversation context from previous run const result = await continueTaskApi(taskId); console.log(`[Mesh] Task continued with ${result.contextEntries} context entries`); setTaskDetail((prev) => prev ? { ...prev, ...result.task } : prev); } } catch (e) { console.error("Failed to continue task:", e); alert(e instanceof Error ? e.message : "Failed to continue task"); } }, [taskDetail?.isSupervisor, taskDetail?.contractId, fetchTask] ); const handleSave = useCallback( async (taskId: string, name: string, description: string, plan: string, targetRepoPath?: string, completionAction?: string) => { if (!taskDetail) return; const result = await editTask(taskId, { name, description: description || undefined, plan, targetRepoPath: targetRepoPath || undefined, completionAction: completionAction as import("../lib/api").CompletionAction | undefined, version: taskDetail.version, }); if (result) { setTaskDetail(result); } }, [editTask, taskDetail] ); const handleBranch = useCallback( async (taskId: string, message: string, name?: string) => { try { const result = await branchTask(taskId, { message, name, includeConversation: true, }); console.log(`[Mesh] Task branched, new task ID: ${result.task.id}`); // Navigate to the new branched task navigate(`/exec/${result.task.id}`); } catch (e) { console.error("Failed to branch task:", e); throw e; // Re-throw so the modal can display the error } }, [navigate] ); // Open contract selection modal const handleCreate = useCallback(async () => { if (creating || contractsLoading) return; setContractsLoading(true); try { const [contractsResponse, directoriesResponse] = await Promise.all([ listContracts(), getDaemonDirectories().catch(() => ({ directories: [] })), ]); setContracts(contractsResponse.contracts); setDaemonDirectories(directoriesResponse.directories); setModalStep(1); setSelectedContract(null); setNewTaskName(""); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); setShowContractModal(true); } catch (e) { console.error("Failed to load contracts:", e); } finally { setContractsLoading(false); } }, [creating, contractsLoading]); // Handle contract selection and move to step 2 const handleSelectContract = useCallback(async (contractSummary: ContractSummary) => { try { const contract = await getContract(contractSummary.id); setSelectedContract(contract); setNewTaskName(`Task for ${contract.name}`); // Pre-select primary repository if available const primaryRepo = contract.repositories.find((r) => r.isPrimary && r.status === "ready"); if (primaryRepo) { setNewTaskRepoUrl(primaryRepo.repositoryUrl); } else { // Otherwise select first ready repository const firstReady = contract.repositories.find((r) => r.status === "ready"); setNewTaskRepoUrl(firstReady?.repositoryUrl || null); } setModalStep(2); } catch (e) { console.error("Failed to load contract details:", e); } }, []); // Handle creating a standalone task (no contract) const handleCreateStandaloneTask = useCallback(() => { setSelectedContract(null); setNewTaskName("Standalone Task"); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); setStandaloneRepoType("remote"); setStandaloneRepoUrl(""); setStandaloneRepoPath(""); setModalStep(2); }, []); // Create task with configured options const handleCreateTask = useCallback(async () => { if (creating) return; setShowContractModal(false); setCreating(true); try { // For standalone tasks, use the standalone repo URL/path based on type let repoUrl = newTaskRepoUrl; let targetPath = newTaskTargetPath; if (!selectedContract) { // Standalone task - use the standalone repo fields if (standaloneRepoType === "remote" && standaloneRepoUrl) { repoUrl = standaloneRepoUrl; } else if (standaloneRepoType === "local" && standaloneRepoPath) { // For local paths, use targetRepoPath instead targetPath = standaloneRepoPath; } } const newTask = await saveTask({ contractId: selectedContract?.id, name: newTaskName || (selectedContract ? `Task for ${selectedContract.name}` : "Standalone Task"), plan: "# Plan\n\nDescribe what this task should accomplish...", repositoryUrl: repoUrl || undefined, targetRepoPath: targetPath || undefined, }); if (newTask) { navigate(`/exec/${newTask.id}`); } } finally { setCreating(false); } }, [creating, saveTask, navigate, selectedContract, newTaskName, newTaskRepoUrl, newTaskTargetPath, standaloneRepoType, standaloneRepoUrl, standaloneRepoPath]); // Close modal and reset state const handleCloseModal = useCallback(() => { setShowContractModal(false); setModalStep(1); setSelectedContract(null); setNewTaskName(""); setNewTaskRepoUrl(null); setNewTaskTargetPath(""); setStandaloneRepoType("remote"); setStandaloneRepoUrl(""); setStandaloneRepoPath(""); setRepoSuggestions([]); setShowRepoSuggestions(false); }, []); // handleTaskUpdatedFromCli + chatContext drove the deleted // UnifiedMeshChatInput; gone with the LLM module. // Handle resizing of the split panel const handleResizeStart = useCallback((e: MouseEvent) => { e.preventDefault(); setIsResizing(true); }, []); useEffect(() => { if (!isResizing) return; const handleMouseMove = (e: globalThis.MouseEvent) => { if (!containerRef.current) return; const containerRect = containerRef.current.getBoundingClientRect(); const containerWidth = containerRect.width; const mouseX = e.clientX - containerRect.left; // Calculate percentage, respecting minimum widths const minTaskPercent = (MIN_TASK_WIDTH / containerWidth) * 100; const maxTaskPercent = ((containerWidth - MIN_OUTPUT_WIDTH) / containerWidth) * 100; const newPercent = Math.max(minTaskPercent, Math.min(maxTaskPercent, (mouseX / containerWidth) * 100)); setTaskPanelPercent(newPercent); }; const handleMouseUp = () => { setIsResizing(false); }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isResizing]); // Cycle through view modes const cycleViewMode = useCallback(() => { setViewMode((current) => { if (current === "split") return "task"; if (current === "task") return "output"; return "split"; }); }, []); // Get label for current view mode const getViewModeLabel = (mode: ViewMode): string => { switch (mode) { case "split": return "Split"; case "task": return "Task"; case "output": return "Output"; } }; // Show loading state while checking auth if (authLoading) { return (
Loading...
); } // Don't render content if not authenticated (redirect will happen via useEffect) if (isAuthConfigured && !isAuthenticated) { return (
Redirecting to login...
); } return (
{error && (
{error}
)} {conflict?.hasConflict && (

Version conflict detected. Please reload and try again.

)} {/* Main content area - conditional based on route */}
{id && taskDetail ? ( <> {/* Header with connection status and view toggle */}
{connected ? "Connected" : "Connecting..."}
{/* View mode toggle */}
{/* Split panel layout */}
{/* Task detail panel */} {(viewMode === "split" || viewMode === "task") && (
navigate(`/contracts/${contractId}`)} onBranch={handleBranch} contractTasks={taskDetail.isSupervisor ? contractTasks : undefined} overlayDiff={overlayDiff} onRequestDiff={handleRequestDiff} />
)} {/* Resizable divider */} {viewMode === "split" && (
)} {/* Output panel */} {(viewMode === "split" || viewMode === "output") && (
{/* Contract complete questions - shown prominently at top */} {contractCompleteQuestionsForTask.length > 0 && (
{contractCompleteQuestionsForTask.map((question) => ( ))}
)}
{ setViewingSubtaskId(null); setViewingSubtaskName(null); } : undefined} onClear={() => { setTaskOutputEntries([]); if (activeOutputTaskId) { clearPersistedOutput(activeOutputTaskId); } }} taskId={activeOutputTaskId} onUserInput={handleUserInput} pendingQuestionIds={pendingQuestionIds} onAnswerQuestion={handleAnswerQuestion} />
)}
) : id && detailLoading ? (
Loading...
) : (
)} {/* UnifiedMeshChatInput removed alongside the LLM module. The mesh page is now a pure task viewer; spawning / editing tasks via natural language chat went with LLM removal. */}
{/* Task Creation Modal (Two Steps) */} {showContractModal && (
{modalStep === 2 && ( )}

{modalStep === 1 ? "Select Contract" : "Configure Task"}

{modalStep === 1 ? ( // Step 1: Select Contract
{/* Standalone task option */}
{/* Contract selection */} {contracts.length === 0 ? (

No contracts found.

) : (
Or select a contract:
{contracts.map((contract) => ( ))}
)}
) : ( // Step 2: Configure Task
{/* Context badge */}
{selectedContract ? ( <> Contract: {selectedContract.name} ) : ( <> Standalone Task )}
{/* Task name */}
setNewTaskName(e.target.value)} 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]" placeholder="Task name" />
{/* Repository selection - for contract tasks */} {selectedContract && selectedContract.repositories.length > 0 && (

The repository this task will work on.

)} {/* Repository selection - for standalone tasks */} {!selectedContract && (
{/* Repository type selector */}
{/* Repository suggestions */} {showRepoSuggestions && repoSuggestions.length > 0 && (
{repoSuggestions.map((suggestion) => ( ))}
)} {/* Repository URL (for remote) */} {standaloneRepoType === "remote" && (
setStandaloneRepoUrl(e.target.value)} placeholder="https://github.com/user/repo.git" 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]" />

The remote repository this task will clone and work on.

)} {/* Repository path (for local) */} {standaloneRepoType === "local" && (

The local directory this task will work in.

)}
)} {/* Target repo path with DirectoryInput - only for contract tasks when repo is selected */} {selectedContract && newTaskRepoUrl && (

Path where the task will push/merge changes. Leave empty to configure later.

)} {/* Create button */}
)}
)}
); }