import "./RoutingTab.css"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { Settings, Task, TaskDetail } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import { fetchNodes, updateTask } from "../api"; import type { NodeInfo } from "../api"; import type { ToastType } from "../hooks/useToast"; import { NodeHealthDot } from "./NodeHealthDot"; interface RoutingTabProps { task: Task | TaskDetail; settings?: Settings; addToast: (message: string, type?: ToastType) => void; onTaskUpdated?: (task: Task) => void; } type RoutingSettings = Settings & { defaultNodeId?: string; unavailableNodePolicy?: "block" | "fallback-local"; }; function getRoutingPolicyLabel(policy: RoutingSettings["unavailableNodePolicy"] | undefined): string { if (policy === "block") return "Block execution"; if (policy === "fallback-local") return "Fall back to local"; return "Not configured"; } const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]); function isUnhealthy(status: NodeInfo["status"] | undefined): boolean { return status !== undefined && status !== "online"; } export function RoutingTab({ task, settings, addToast, onTaskUpdated }: RoutingTabProps) { const [nodes, setNodes] = useState([]); const [loadingNodes, setLoadingNodes] = useState(false); const [nodesError, setNodesError] = useState(null); const [selectedNodeId, setSelectedNodeId] = useState(task.nodeId ?? ""); const [savingNode, setSavingNode] = useState(false); const activeTaskIdRef = useRef(task.id); useEffect(() => { activeTaskIdRef.current = task.id; setSelectedNodeId(task.nodeId ?? ""); setSavingNode(false); }, [task.id, task.nodeId]); useEffect(() => { setLoadingNodes(true); setNodesError(null); fetchNodes() .then((result) => { setNodes(result); }) .catch((err) => { setNodesError(getErrorMessage(err) || "Failed to load nodes"); }) .finally(() => { setLoadingNodes(false); }); }, []); const nodesById = useMemo(() => new Map(nodes.map((node) => [node.id, node])), [nodes]); const sortedNodes = useMemo( () => [...nodes].sort((a, b) => a.name.localeCompare(b.name)), [nodes], ); const routingSettings = settings as RoutingSettings | undefined; const effectiveNodeId = task.nodeId ?? routingSettings?.defaultNodeId ?? null; const routingSource = task.nodeId ? "Per-task override" : routingSettings?.defaultNodeId ? "Project default" : "No routing"; const effectiveNode = effectiveNodeId ? nodesById.get(effectiveNodeId) : undefined; const effectiveNodeName = effectiveNode ? `${effectiveNode.name} (${effectiveNode.type})` : effectiveNodeId ? `${effectiveNodeId} (node unavailable or unknown)` : "Local (no routing configured)"; const isTaskActive = task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string); const selectorDisabled = isTaskActive || savingNode || loadingNodes; const handleNodeSelect = useCallback( async (nextValue: string) => { if (nextValue === selectedNodeId) { return; } const requestTaskId = task.id; const previousValue = selectedNodeId; setSelectedNodeId(nextValue); setSavingNode(true); try { const updatedTask = await updateTask(requestTaskId, { nodeId: nextValue || null }); if (activeTaskIdRef.current !== requestTaskId) return; setSelectedNodeId(updatedTask.nodeId ?? ""); onTaskUpdated?.(updatedTask); addToast(nextValue ? "Node override updated" : "Node override cleared", "success"); } catch (err) { if (activeTaskIdRef.current !== requestTaskId) return; setSelectedNodeId(previousValue); addToast(getErrorMessage(err) || "Failed to update node override", "error"); } finally { if (activeTaskIdRef.current === requestTaskId) { setSavingNode(false); } } }, [addToast, onTaskUpdated, selectedNodeId, task.id], ); const clearOverride = useCallback(() => { void handleNodeSelect(""); }, [handleNodeSelect]); return (

Task Routing

View the effective execution node and control per-task node override.

Routing Summary
Effective node {effectiveNode ? : null} {effectiveNodeName} {isUnhealthy(effectiveNode?.status) ? ( Unhealthy ) : null}
Routing source {routingSource}
Unavailable-node policy {getRoutingPolicyLabel(routingSettings?.unavailableNodePolicy)}
{isTaskActive && effectiveNodeId ? (
Routing is locked while this task is active. Node override cannot be changed until the task is no longer active.
) : null}
Node Override
{isTaskActive ? (
Node override cannot be changed while the task is active.
) : null} {nodesError ?
{nodesError}
: null} {task.nodeId ? (
Override set to: {nodesById.get(task.nodeId)?.name ?? task.nodeId}
) : null}
); }