import { useNavigate } from "@tanstack/react-router"; import { CalendarClockIcon, PlayIcon, Settings2Icon } from "lucide-react"; import { useState } from "react"; import type { EnvironmentId, ScheduledTask, ThreadId } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { cn } from "../../lib/utils"; import { relativeLabel, scheduleLabel } from "../settings/ScheduledTasksSettings"; import { useEnvironmentQuery } from "../../state/query"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { Button } from "../ui/button"; import { Switch } from "../ui/switch"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { THREAD_DETAILS_PANEL_ICON_ACTION_CLASS, THREAD_DETAILS_PANEL_ICON_CLASS, } from "./threadDetailsPanelStyles"; const STATUS_DOT_CLASS: Record = { never: "bg-muted-foreground/40", running: "animate-pulse bg-sky-500", succeeded: "bg-emerald-500", failed: "bg-destructive", }; /** * Thread details panel section listing the automations (scheduled tasks) bound * to this thread. Fed by the live scheduled-task subscription, so run status * and next-run times update as the scheduler works — no manual refresh. * Renders nothing when the thread has no bound automations. */ export function ThreadAutomationsPanel(props: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; }) { const tasksQuery = useEnvironmentQuery( serverEnvironment.scheduledTasksLive({ environmentId: props.environmentId, input: {} }), ); const setTaskEnabled = useAtomCommand(serverEnvironment.setScheduledTaskEnabled, { label: "thread automation toggle", }); const runTaskNow = useAtomCommand(serverEnvironment.runScheduledTaskNow, { label: "thread automation run now", }); const navigate = useNavigate(); const [busyTaskId, setBusyTaskId] = useState(null); const boundTasks = (tasksQuery.data?.tasks ?? []).filter( (task) => task.threadId === props.threadId, ); // A load error must not look like "no automations" — this thread may have // tasks whose controls would silently vanish. Only hide the section when we // positively know there is nothing bound to it. if (tasksQuery.error === null && boundTasks.length === 0) return null; const reportFailure = (title: string, error: unknown) => { toastManager.add( stackedThreadToast({ type: "error", title, description: error instanceof Error ? error.message : String(error), }), ); }; const toggleEnabled = async (task: ScheduledTask, enabled: boolean) => { if (busyTaskId !== null) return; setBusyTaskId(task.id); // Partial update: only the enabled flag changes, so a toggle can never // revert concurrent edits made to the task elsewhere. const result = await setTaskEnabled({ environmentId: props.environmentId, input: { id: task.id, enabled }, }); setBusyTaskId(null); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportFailure("Could not update automation", squashAtomCommandFailure(result)); } }; const runNow = async (task: ScheduledTask) => { if (busyTaskId !== null) return; setBusyTaskId(task.id); const result = await runTaskNow({ environmentId: props.environmentId, input: { id: task.id }, }); setBusyTaskId(null); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportFailure("Could not run automation", squashAtomCommandFailure(result)); } }; return (

Automations

void navigate({ to: "/settings/scheduled-tasks" })} > } /> Manage schedule tasks
{tasksQuery.error !== null ? (

Could not load automations: {tasksQuery.error}

) : null}
    {boundTasks.map((task) => (
  • {task.title}

    {scheduleLabel(task.schedule)} {task.enabled && task.nextRunAt !== null ? ` · next ${relativeLabel(task.nextRunAt)}` : task.enabled ? "" : " · paused"}

    void runNow(task)} > } /> Run now void toggleEnabled(task, enabled)} />
  • ))}
); }