import { useCallback, useEffect, useState } from "react"; import type { Project, ScheduledTask, SchedulerNotification } from "../../../lib/types"; import { clearSchedulerNotifications, getScheduledTaskLog, getSchedulerNotifications, listScheduledTasks, removeScheduledTask, runScheduledTaskNow, setScheduledTaskEnabled, } from "../../../lib/tauri-commands"; import { useAppState } from "../../../store/appState"; import Button from "../../ui/Button"; import Toggle from "../../ui/Toggle"; import Modal from "../../ui/Modal"; import StatusIndicator from "../../ui/StatusIndicator"; import TaskEditorModal from "./TaskEditorModal"; import { formatAge, formatRunningFor } from "./format"; interface Props { project: Project; } /** * UI for `triple-c-scheduler`, which ships in every container and until now * had no interface beyond a CLAUDE.md paragraph. */ export default function AutomationTab({ project }: Props) { const [tasks, setTasks] = useState([]); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const [busyTaskId, setBusyTaskId] = useState(null); const [log, setLog] = useState<{ task: ScheduledTask; text: string } | null>(null); const [confirmRemoveId, setConfirmRemoveId] = useState(null); /** `undefined` = closed, `null` = creating, a task = editing it. */ const [editing, setEditing] = useState(undefined); const pushToast = useAppState((s) => s.pushToast); const running = project.status === "running"; const load = useCallback(() => { if (!running) { setTasks([]); setNotifications([]); return; } setLoading(true); Promise.all([ listScheduledTasks(project.id).catch(() => [] as ScheduledTask[]), getSchedulerNotifications(project.id).catch( () => [] as SchedulerNotification[], ), ]) .then(([t, n]) => { setTasks(t); setNotifications(n); }) .finally(() => setLoading(false)); }, [project.id, running]); useEffect(load, [load]); // A task in flight is the one state this view cannot sit still for: runs are // detached, so without polling "Run now" looks like it did nothing until the // user reaches for Refresh. Polling stops as soon as nothing is running. // // `justTriggered` covers the gap between firing a run and the runner writing // its state file — a second or two in which the task still reads as idle, and // where giving up on polling would reproduce the exact silence this fixes. const anyTaskRunning = tasks.some((t) => t.running); const [justTriggered, setJustTriggered] = useState(0); useEffect(() => { if (!running) return; if (!anyTaskRunning && Date.now() - justTriggered > 20_000) return; const timer = setInterval(load, anyTaskRunning ? 5000 : 1500); return () => clearInterval(timer); }, [running, anyTaskRunning, justTriggered, load]); const withTask = async (taskId: string, label: string, fn: () => Promise) => { setBusyTaskId(taskId); try { await fn(); load(); } catch (e) { pushToast({ kind: "error", message: `${label} failed`, detail: String(e) }); } finally { setBusyTaskId(null); } }; const openLog = async (task: ScheduledTask) => { setBusyTaskId(task.id); try { const text = await getScheduledTaskLog(project.id, task.id, 200); setLog({ task, text }); } catch (e) { pushToast({ kind: "error", message: `Could not read the log for “${task.name}”`, detail: String(e), }); } finally { setBusyTaskId(null); } }; const removing = tasks.find((t) => t.id === confirmRemoveId) ?? null; return (
{/* Notifications */} {notifications.length > 0 && (

{notifications.length} notification {notifications.length === 1 ? "" : "s"}

    {notifications.map((n, i) => (
  • {n.task_name ?? n.task_id} {n.status && ( )} {formatAge(n.created_at) ?? n.time ?? ""}

    {n.summary ?? n.body}

  • ))}
)}

Recurring Claude Code runs managed by{" "} triple-c-scheduler {" "} inside the container.

{!running ? (

Start the container to list its scheduled tasks.

) : tasks.length === 0 && !loading ? (

No scheduled tasks yet. Use New task, or ask Claude to add one with{" "} triple-c-scheduler add.

) : (
    {tasks.map((task) => (
  • {task.name} {task.task_type} {task.running && ( )}
    {task.at ?? task.schedule} {task.last_run ? ` · last run ${formatAge(task.last_run) ?? task.last_run}` : ""}
    withTask(task.id, "Toggle task", () => setScheduledTaskEnabled(project.id, task.id, v), ) } />
  • ))}
)}
{editing !== undefined && ( setEditing(undefined)} onSaved={load} /> )} {log && ( setLog(null)} widthClassName="w-[46rem]" footer={} >
            {log.text.trim() || "(empty log)"}
          
)} {removing && ( setConfirmRemoveId(null)} widthClassName="w-[26rem]" footer={ <> } >

Remove {removing.name}{" "} from this container’s scheduler?

)}
); }