import "./ExecutorStatusBar.css"; import { useMemo, useState } from "react"; import type { Task } from "@fusion/core"; import { AlertTriangle, Clock, Folder, Pause, Play, Zap } from "lucide-react"; 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 StateIcon = stateDisplay.icon; if (error) { return (
{error}
); } if (loading && stats.runningTaskCount === 0) { return (
Loading...
); } return (
{/* Background AI tasks indicator */} {backgroundSessions && backgroundSessions.length > 0 && onOpenBackgroundSession && onDismissBackgroundSession && ( <>