import { basename } from "node:path"; import { useKeyboard, useRenderer } from "@opentui/react"; import { useCallback, useRef, useState } from "react"; import { resolveDashboardKeyAction } from "@/lib/tui/dashboard/dashboard-keyboard"; import { TUI_COLORS } from "@/lib/tui/shared/colors"; import { SelectionCopyToastBoundary } from "@/lib/tui/shared/SelectionCopyToastBoundary"; import type { DashboardProps } from "@/lib/tui/shared/types"; import { useWorkspaceState } from "@/lib/tui/workspace/use-workspace-state"; import { Workspace } from "@/lib/tui/workspace/Workspace"; import type { FocusedPane } from "@/lib/tui/workspace/workspace-types"; import { DashboardOverlays } from "./DashboardOverlays"; import { getPendingFixTarget } from "./dashboard-fix-state"; import { cycleDashboardFocus, cycleDashboardFocusReverse } from "./dashboard-focus"; import { resolveSelectedGroupPath, selectAdjacentGroupPath } from "./dashboard-group-selection"; import { isDashboardOverlayBlockingFocus } from "./dashboard-overlay-state"; import { Header } from "./Header"; import { StatusBar } from "./StatusBar"; import { useDashboardRunControl } from "./use-dashboard-run-control"; import { useDashboardStopControl } from "./use-dashboard-stop-control"; export function Dashboard({ projectPath, branch, refreshInterval = 1000, deps }: DashboardProps) { const renderer = useRenderer(); const resolvedUseWorkspaceState = deps?.useWorkspaceState ?? useWorkspaceState; const ResolvedDashboardOverlays = deps?.DashboardOverlays ?? DashboardOverlays; const [selectedGroupPath, setSelectedGroupPath] = useState(projectPath); const state = resolvedUseWorkspaceState( projectPath, branch, refreshInterval, undefined, selectedGroupPath ); const { runError, startupMode, clearRunError, clearRunStartState, setRunError, spawnRunProcess, spawnFixProcess, isStartupSpawning, } = useDashboardRunControl(projectPath, { spawn: deps?.spawn }); const [focusedPane, setFocusedPane] = useState("detail"); const [outputVisible, setOutputVisible] = useState(false); const [showHelp, setShowHelp] = useState(false); const [showFixFindings, setShowFixFindings] = useState(false); const [showSession, setShowSession] = useState(false); const [showReviewModeOverlay, setShowReviewModeOverlay] = useState(false); const [showStopPicker, setShowStopPicker] = useState(false); const { isStoppingRun, stopSelectedSession } = useDashboardStopControl( { currentSession: state.currentSession, setShowStopPicker, onError: setRunError, }, { stopActiveSession: deps?.stopActiveSession } ); const projectName = basename(projectPath); const isExitingRef = useRef(false); const pendingFixTarget = getPendingFixTarget( state.lastSessionStats, state.storedFindings, state.unselectedFindings, state.unresolvedSelectedFindings ); const canFixPendingSession = pendingFixTarget !== null; const [prevCurrentSessionId, setPrevCurrentSessionId] = useState( state.currentSession?.sessionId ?? null ); const currentSessionId = state.currentSession?.sessionId ?? null; if (currentSessionId !== prevCurrentSessionId) { setPrevCurrentSessionId(currentSessionId); if (currentSessionId !== null) { clearRunStartState(); setShowReviewModeOverlay(false); setShowFixFindings(false); } } const [prevProjectSessionsCount, setPrevProjectSessionsCount] = useState( state.projectSessions.length ); if (state.projectSessions.length !== prevProjectSessionsCount) { setPrevProjectSessionsCount(state.projectSessions.length); if (state.projectSessions.length <= 1) { setShowStopPicker(false); } } if (showFixFindings && !canFixPendingSession) { setShowFixFindings(false); } const resolvedSelectedGroupPath = resolveSelectedGroupPath( state.sessionGroups, selectedGroupPath, projectPath ); if (resolvedSelectedGroupPath !== selectedGroupPath) { setSelectedGroupPath(resolvedSelectedGroupPath); } const shutdown = useCallback( async (after?: () => Promise, exitCode: number = 0) => { if (isExitingRef.current) { return; } isExitingRef.current = true; renderer.stop(); const waitForDestroy = renderer.isDestroyed ? Promise.resolve() : new Promise((resolve) => { renderer.once("destroy", () => resolve()); }); renderer.destroy(); await waitForDestroy; if (after) { try { await after(); } catch { exitCode = 1; } } process.exitCode = exitCode; }, [renderer] ); const cycleFocus = useCallback(() => { setFocusedPane((current) => cycleDashboardFocus(current, outputVisible)); }, [outputVisible]); const cycleFocusReverse = useCallback(() => { setFocusedPane((current) => cycleDashboardFocusReverse(current, outputVisible)); }, [outputVisible]); const selectPrevGroup = useCallback(() => { setSelectedGroupPath((current) => selectAdjacentGroupPath(state.sessionGroups, current, "prev") ); }, [state.sessionGroups]); const selectNextGroup = useCallback(() => { setSelectedGroupPath((current) => selectAdjacentGroupPath(state.sessionGroups, current, "next") ); }, [state.sessionGroups]); const handleKeyboard = useCallback( (key: { name: string }) => { const action = resolveDashboardKeyAction({ keyName: key.name, showStopPicker, showHelp, showRunOverlay: showReviewModeOverlay, showFixFindings, showSession, activeSessionCount: state.allSessions.length, hasCurrentSession: Boolean(state.currentSession), canFixPendingSession, isRunSpawning: isStartupSpawning(), sidebarFocused: focusedPane === "sidebar", sessionGroupCount: state.sessionGroups.length, }); switch (action) { case "close-stop-picker": setShowStopPicker(false); return; case "close-help": setShowHelp(false); return; case "delegate-run-overlay": case "delegate-fix-overlay": case "delegate-session-overlay": case "none": return; case "shutdown": void shutdown(); return; case "cycle-focus": cycleFocus(); return; case "cycle-focus-reverse": cycleFocusReverse(); return; case "toggle-output": setOutputVisible((current) => !current); return; case "open-help": setShowHelp(true); return; case "open-session": setShowSession(true); return; case "open-fix-findings": setShowSession(false); setShowFixFindings(true); return; case "stop-single-session": { const target = state.allSessions[0]; if (target) { void stopSelectedSession(target); } return; } case "open-stop-picker": setShowStopPicker(true); return; case "open-review-mode": clearRunError(); setShowReviewModeOverlay(true); return; case "select-prev-group": selectPrevGroup(); return; case "select-next-group": selectNextGroup(); return; } }, [ canFixPendingSession, clearRunError, cycleFocus, cycleFocusReverse, focusedPane, isStartupSpawning, selectNextGroup, selectPrevGroup, showFixFindings, showHelp, showReviewModeOverlay, showSession, showStopPicker, shutdown, state.allSessions, state.currentSession, state.sessionGroups.length, stopSelectedSession, ] ); useKeyboard(handleKeyboard); const displayError = state.error || runError; const showRunOverlay = showReviewModeOverlay && !state.currentSession; const isOverlayBlocked = isDashboardOverlayBlockingFocus({ showHelp, showRunOverlay, showFixFindings, showSession, showStopPicker, }); const hasSession = !displayError && Boolean(state.currentSession); return (
{displayError ? ( Error: {displayError} ) : ( )} setShowHelp(false)} onCloseRunOverlay={() => setShowReviewModeOverlay(false)} onSubmitRunOverlay={(args) => { setShowReviewModeOverlay(false); spawnRunProcess(args); }} onCloseFixFindings={() => setShowFixFindings(false)} onSubmitFixOverlay={(args) => { setShowFixFindings(false); spawnFixProcess(args); }} onCloseSession={() => setShowSession(false)} onSelectStopSession={(session) => { void stopSelectedSession(session); }} onCloseStopPicker={() => setShowStopPicker(false)} /> ); }