import "./ExecutorStatusBar.css"; import { useMemo, useState } from "react"; import { HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, type Task } from "@fusion/core"; import { AlertTriangle, Clock, Folder, Pause, Play, Zap } from "lucide-react"; import { computeBlockerFanoutMap } from "../hooks/useBlockerFanout"; import { useExecutorStats } from "../hooks/useExecutorStats"; import type { ExecutorState, AiSessionSummary } from "../api"; import { BackgroundTasksIndicator } from "./BackgroundTasksIndicator"; interface ExecutorStatusBarProps { /** Task list (shared with the board to keep counts in sync) */ tasks: Task[]; /** Project ID for fetching project-specific stats */ projectId?: string; /** Project-level stuck task timeout in milliseconds (undefined = disabled) */ taskStuckTimeoutMs?: number; /** Background AI sessions */ backgroundSessions?: AiSessionSummary[]; backgroundGenerating?: number; backgroundNeedsInput?: number; onOpenBackgroundSession?: (session: AiSessionSummary) => void; onDismissBackgroundSession?: (id: string) => void; /** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */ lastFetchTimeMs?: number; /** Absolute path for the currently selected project directory. */ currentProjectPath?: string; /** Opens the workspace-aware file browser to the project workspace. */ onOpenProjectDirectory?: () => void; /** When true on mobile, hide the bar so it doesn't slide over messages * during visualViewport pans (position:fixed is anchored to layout * viewport, which iOS leaves below the keyboard). */ keyboardOpen?: boolean; } /** * Format a relative time string (e.g., "2m ago", "1h ago") */ function formatRelativeTime(timestamp: string | undefined): string { if (!timestamp) return "no activity"; const now = Date.now(); const then = new Date(timestamp).getTime(); const diffMs = now - then; const seconds = Math.floor(diffMs / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ago`; if (hours > 0) return `${hours}h ago`; if (minutes > 0) return `${minutes}m ago`; if (seconds > 10) return `${seconds}s ago`; return "just now"; } /** * Get display configuration for an executor state */ function getStateDisplay(state: ExecutorState): { label: string; color: string; icon: typeof Play } { switch (state) { case "running": return { label: "Running", color: "var(--color-success)", icon: Play }; case "paused": return { label: "Paused", color: "var(--triage)", icon: Pause }; case "idle": default: return { label: "Idle", color: "var(--text-muted)", icon: Zap }; } } /** * Footer status bar component that displays real-time executor statistics. * * Shows: * - Running tasks count with pulsing animation when > 0 * - Blocked tasks count with warning color when > 0 * - Queued tasks count * - Executor state badge (idle/running/paused) * - Last activity timestamp */ export function ExecutorStatusBar({ tasks, projectId, taskStuckTimeoutMs, backgroundSessions, backgroundGenerating, backgroundNeedsInput, onOpenBackgroundSession, onDismissBackgroundSession, lastFetchTimeMs, currentProjectPath, onOpenProjectDirectory, keyboardOpen }: ExecutorStatusBarProps) { if (keyboardOpen) return null; const { stats, loading, error } = useExecutorStats(tasks, projectId, taskStuckTimeoutMs, lastFetchTimeMs); const [isProjectPathVisible, setIsProjectPathVisible] = useState(false); const stateDisplay = useMemo(() => getStateDisplay(stats.executorState), [stats.executorState]); const relativeTime = useMemo(() => formatRelativeTime(stats.lastActivityAt), [stats.lastActivityAt]); const highestFanoutBlocker = useMemo(() => { const fanoutMap = computeBlockerFanoutMap(tasks); const candidates = tasks .filter((task) => task.column === "in-progress" || task.column === "in-review") .map((task) => { const fanout = fanoutMap.get(task.id); if (!fanout || !fanout.isHighFanout) return null; return { id: task.id, activeTodoCount: fanout.activeTodoCount, totalCount: fanout.totalCount, staleCount: fanout.staleBlockedByDependentIds.length, }; }) .filter((entry): entry is { id: string; activeTodoCount: number; totalCount: number; staleCount: number } => Boolean(entry)) .sort((a, b) => { if (b.activeTodoCount !== a.activeTodoCount) return b.activeTodoCount - a.activeTodoCount; if (b.totalCount !== a.totalCount) return b.totalCount - a.totalCount; return a.id.localeCompare(b.id, "en", { numeric: true, sensitivity: "base" }); }); return candidates[0] ?? null; }, [tasks]); const StateIcon = stateDisplay.icon; if (error) { return (