import { ModelSettings } from "./components/ModelSettings"; import { Alert, ActionIcon, AppShell, Box, Button, CloseButton, Container, Group, MantineProvider, Paper, SegmentedControl, Select, Stack, Text, Title, Tooltip, createTheme, useComputedColorScheme, useMantineColorScheme, } from "@mantine/core"; import { IconArrowLeft, IconSettings, IconAlertTriangle, IconCircleFilled, IconMoon, IconSun } from "@tabler/icons-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { dismissNotification, filterTasks, manualTransitionExpectedState, reconcileManualTransitionResponse, reconcileNotifications, statusFilterCounts, STATUS_FILTERS, } from "../state.js"; import { connectDashboard, dashboardVersion, manualTransition, openInCodex, taskDetail } from "./api"; import { clearNotificationHistory, listUsageSignature, mergeListTaskIntoDetail, notificationClicked, } from "./overlay-state"; import { usageStillCalculating } from "./presentation"; import type { DashboardSnapshot, NotificationSnapshot, Task } from "./types"; import { NotificationCenter } from "./components/NotificationCenter"; import { TaskCard } from "./components/TaskCard"; import { TaskBoard } from "./components/TaskBoard"; import { TaskDetail } from "./components/TaskDetail"; import { RelativeTimeProvider } from "./components/RelativeTime"; const theme = createTheme({ primaryColor: "teal", colors: { teal: [ "#e6fcf5", "#c3fae8", "#96f2d7", "#63e6be", "#38d9a9", "#20c997", "#087f5b", "#067052", "#055e46", "#044d39", ], }, defaultRadius: "md", fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif", headings: { fontFamily: "inherit", fontWeight: "680" }, components: { Button: { defaultProps: { radius: "md" } }, Paper: { defaultProps: { radius: "md" } }, Select: { defaultProps: { size: "xs" } }, }, }); const styleNonce = document.querySelector('meta[name="taskchef-style-nonce"]')?.content; const VIEW_STORAGE_KEY = "taskchef.dashboard.view"; function savedView(): "board" | "list" { try { return window.localStorage.getItem(VIEW_STORAGE_KEY) === "board" ? "board" : "list"; } catch { return "list"; } } interface NotificationState { initialized: boolean; announcements: NotificationSnapshot[]; notifications: NotificationSnapshot[]; seenIds: Set; signatures: Map; } export function TaskResultsSummary({ totalCount, visibleCount, }: { totalCount: number; visibleCount: number; }) { return ( Tasks: {visibleCount} of {totalCount} ); } export function DashboardApp({ connect = true, initialFilters = {}, initialTasks = [], }: { connect?: boolean; initialFilters?: { project?: string; status?: string }; initialTasks?: Task[]; }) { const [settings, setSettings] = useState(() => window.location.hash === '#settings'); useEffect(() => { const update = () => setSettings(window.location.hash === '#settings'); window.addEventListener('hashchange', update); return () => window.removeEventListener('hashchange', update); }, []); const [tasks, setTasks] = useState(initialTasks); const [projectIndex, setProjectIndex] = useState(); const [connected, setConnected] = useState(false); const [message, setMessage] = useState(null); const [version, setVersion] = useState(null); const [project, setProject] = useState(initialFilters.project ?? ""); const [status, setStatus] = useState(initialFilters.status ?? ""); const [date, setDate] = useState("all"); const [preferredView, setPreferredView] = useState(savedView); const [completedLimit, setCompletedLimit] = useState(5); const [now, setNow] = useState(() => Date.now()); const [selectedTask, setSelectedTask] = useState(null); const [detailOpened, setDetailOpened] = useState(false); const [detailBusy, setDetailBusy] = useState(false); const [detailError, setDetailError] = useState(null); const [detailNotice, setDetailNotice] = useState(null); const [highlightTurnRef, setHighlightTurnRef] = useState(null); const [notificationState, setNotificationState] = useState({ initialized: initialTasks.length > 0, announcements: [], notifications: [], seenIds: new Set(), signatures: new Map(), }); const selectedId = selectedTask?.id ?? null; const selectedIdRef = useRef(selectedId); selectedIdRef.current = selectedId; const detailGeneration = useRef(0); const detailListUpdatedAt = useRef(null); const detailListUsageSignature = useRef(listUsageSignature(null)); const applySnapshot = useCallback((snapshot: DashboardSnapshot) => { setTasks(snapshot.tasks); setProjectIndex(snapshot.projectIndex); setNotificationState((current) => { const reconciled = reconcileNotifications(current, snapshot.tasks); return { initialized: true, announcements: reconciled.additions, notifications: reconciled.notifications, seenIds: reconciled.seenIds, signatures: reconciled.signatures, }; }); setMessage(snapshot.healthy === false ? "The task log is temporarily unavailable. Showing the last valid snapshot." : null); const currentSelectedId = selectedIdRef.current; if (currentSelectedId) { const updated = snapshot.tasks.find((task) => task.id === currentSelectedId); if (updated) { setSelectedTask((current) => current ? mergeListTaskIntoDetail(current, updated) : updated); } } }, []); useEffect(() => { const timer = window.setInterval(() => setNow(Date.now()), 30_000); return () => window.clearInterval(timer); }, []); useEffect(() => { if (!connect) return; void dashboardVersion().then(setVersion).catch(() => setVersion(null)); const stream = connectDashboard({ onConnection: setConnected, onError: setMessage, onSnapshot: applySnapshot, }); return () => stream.close(); }, [applySnapshot, connect]); const loadDetail = useCallback(async (task: Task, focusTurnRef: string | null = null) => { const generation = ++detailGeneration.current; detailListUpdatedAt.current = task.updatedAt; detailListUsageSignature.current = listUsageSignature(task); setSelectedTask((current) => current?.id === task.id ? mergeListTaskIntoDetail(current, task) : task); setDetailOpened(true); setHighlightTurnRef(focusTurnRef); setDetailError(null); setDetailNotice(null); try { for (let attempt = 0; attempt <= 40; attempt += 1) { if (generation !== detailGeneration.current) return; const detail = await taskDetail(task.id); if (generation !== detailGeneration.current) return; detailListUpdatedAt.current = detail.updatedAt; detailListUsageSignature.current = listUsageSignature(detail); setSelectedTask(detail); if (!usageStillCalculating(detail) || attempt === 40) return; await new Promise((resolve) => window.setTimeout(resolve, 1_500)); } } catch { if (generation === detailGeneration.current) { setDetailError("Task activity is temporarily unavailable. Showing the latest list snapshot."); } } }, []); const selectedListTask = tasks.find((task) => task.id === selectedId) ?? null; const selectedListUsageSignature = listUsageSignature(selectedListTask); useEffect(() => { if (!detailOpened || !selectedId || !connect) return; if (!selectedListTask) return; if ( selectedListTask.updatedAt === detailListUpdatedAt.current && selectedListUsageSignature === detailListUsageSignature.current ) return; void loadDetail(selectedListTask, highlightTurnRef); // Task or cached usage changes while detail is open refresh the full projection. // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedListTask?.updatedAt, selectedListUsageSignature]); const projects = useMemo(() => [ { label: "All projects", value: "" }, ...[...new Set(tasks.map((task) => task.project.name))].sort().map((value) => ({ label: value, value })), ], [tasks]); const visible = useMemo(() => filterTasks(tasks, { project, status, date, now }), [tasks, project, status, date, now]); const boardTasks = useMemo(() => filterTasks(tasks, { project, date, now }), [tasks, project, date, now]); const board = preferredView === "board"; const listLayout = !board && !settings; const counts = useMemo(() => statusFilterCounts(tasks, { project, date, now }), [tasks, project, date, now]); const statusData = STATUS_FILTERS.map(({ label, value }: { label: string; value: string }) => ({ label: counts[value] > 0 ? `${label} ${counts[value]}` : label, value, })); function changeView(value: string) { if (value !== "board" && value !== "list") return; setPreferredView(value); try { window.localStorage.setItem(VIEW_STORAGE_KEY, value); } catch { /* Preference remains in memory. */ } } async function handleOpenCodex(task: Task) { setDetailBusy(true); try { const result = await openInCodex(task.id); if (result) setMessage(result); } catch { setMessage("Codex could not be opened. Open the project and select the recorded task instead."); } finally { setDetailBusy(false); } } async function handleTransition(targetStatus: "completed" | "failed", actionId: string) { if (!selectedTask) return { ok: false }; const requestTask = selectedTask; const expected = manualTransitionExpectedState(requestTask); setDetailBusy(true); setDetailError(null); const result = await manualTransition(requestTask, targetStatus, actionId); setDetailBusy(false); if (result.task) { setSelectedTask((current) => reconcileManualTransitionResponse({ requestTask, expected, responseTask: result.task, selectedTask: current, })); setTasks((current) => current.map((candidate) => candidate.id === requestTask.id ? reconcileManualTransitionResponse({ requestTask, expected, responseTask: result.task, selectedTask: candidate, }) : candidate)); } if (result.ok && result.task) { setHighlightTurnRef(result.task.turnRef ?? result.task.turnId); return { ok: true }; } else { setDetailError(result.message ?? "Task state could not be changed."); return { ok: false, rotateActionId: result.code === "stale_task" }; } } function handleNotificationOpen(notification: NotificationSnapshot) { const currentTask = tasks.find((task) => task.id === notification.taskId); const overlay = notificationClicked({ detailTaskId: selectedId, detailOpened, highlightTurnRef, notifications: notificationState.notifications, refreshGeneration: detailGeneration.current, }, notification, Boolean(currentTask)); setNotificationState((current) => ({ ...current, notifications: overlay.notifications })); if (!currentTask) { setMessage("This task is no longer present in the current snapshot."); return; } void loadDetail(currentTask, overlay.highlightTurnRef); } return ( styleNonce : undefined} theme={theme} > <span>TaskChef Dashboard</span> {version && <span className="taskchef-version">v{version}</span>} {connected ? "Live" : connect ? "Connecting…" : "Fixture preview"} {projectIndex?.status === "unavailable" && ( } mb="md" title="Project index unavailable"> Task history is still visible. Verify the index and inspect backups before making project changes. )} {projectIndex?.status === "available" && projectIndex.missingProjects.length > 0 && ( } mb="md" title="Historical projects are missing from the current index"> {projectIndex.missingProjects.map((item) => ( {item.snapshotNames.join(", ")}: {item.taskCount} task(s). Run taskchef backup list --json and preview any restore; the dashboard never restores automatically. ))} )} {settings ? : <> { setDate(value ?? "all"); setCompletedLimit(5); }} value={date} /> {!board && } {!board && } {message && ( } mt="md" role="status"> {message} setMessage(null)} size="sm" /> )} {board ? setCompletedLimit((limit) => limit + 5)} onOpenCodex={handleOpenCodex} onOpenDetail={(value) => void loadDetail(value)} tasks={boardTasks} /> : {visible.map((task: Task) => ( void loadDetail(value)} task={task} /> ))} {visible.length === 0 && ( No tasks match these filters Choose a different project, update window, or status. )} } } { if (!detailBusy) { detailGeneration.current += 1; setDetailOpened(false); setHighlightTurnRef(null); } }} onCopy={() => { if (!selectedTask) return; setDetailNotice(null); void navigator.clipboard.writeText(selectedTask.id).then( () => setDetailNotice("Task ID copied."), () => setDetailNotice("Clipboard access is unavailable. Copy the Task ID from metadata."), ); }} onOpenCodex={() => selectedTask && void handleOpenCodex(selectedTask)} onTransition={handleTransition} opened={detailOpened} task={selectedTask} notice={detailNotice} notifications={detailOpened ? ( setNotificationState((current) => ({ ...current, notifications: clearNotificationHistory({ detailTaskId: selectedId, detailOpened, highlightTurnRef, notifications: current.notifications, refreshGeneration: detailGeneration.current, }).notifications, }))} onDismiss={(notification) => setNotificationState((current) => ({ ...current, notifications: dismissNotification(current.notifications, notification.id) }))} onOpen={handleNotificationOpen} tasks={tasks} withinPortal={false} /> ) : null} /> {!detailOpened && ( setNotificationState((current) => ({ ...current, notifications: clearNotificationHistory({ detailTaskId: selectedId, detailOpened, highlightTurnRef, notifications: current.notifications, refreshGeneration: detailGeneration.current, }).notifications, }))} onDismiss={(notification) => setNotificationState((current) => ({ ...current, notifications: dismissNotification(current.notifications, notification.id) }))} onOpen={handleNotificationOpen} tasks={tasks} /> )} ); } function ThemeToggle() { const { setColorScheme } = useMantineColorScheme(); const colorScheme = useComputedColorScheme("light"); const next = colorScheme === "dark" ? "light" : "dark"; return ( setColorScheme(next)} size="lg" variant="subtle" > {colorScheme === "dark" ? : } ); } function BrandIcon() { const colorScheme = useComputedColorScheme("light"); return ( ); } export default DashboardApp;