Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558import { useCustomerQuerySession } from "../runtime/customer-query-provider";import { LoadingState } from "../components/loading-state";import { ClockIcon } from "@octanejs/phosphor-icons";import { Link, useParams } from "@octanejs/tanstack-router";import { useEffect, useMemo, useRef, useState, useSyncExternalStore,} from "octane";import { Button } from "octane-kumo/components/button";import { Empty } from "octane-kumo/components/empty";import { Loader } from "octane-kumo/components/loader";import { LayerCard } from "octane-kumo/components/layer-card";import { Dialog } from "octane-kumo/components/dialog";import { Input, InputArea } from "octane-kumo/components/input";import { Select } from "octane-kumo/components/select";import { Switch } from "octane-kumo/components/switch";import { MAX_TASKS, MAX_TASK_NAME, MAX_TASK_INSTRUCTIONS, type TaskRunStatus, type TaskSummary,} from "../../shared/tasks";import { TaskSession } from "../runtime/task-session";import { activeRun, emptyTaskHistory } from "../runtime/task-history";import { useTaskListQuery } from "../runtime/queries/use-task-list-query";import { useTaskRunsQuery } from "../runtime/queries/use-task-runs-query";import { useShellSession } from "../runtime/shell-session";
const kinds = [ { value: "cron", label: "Recurring" }, { value: "once", label: "One time" },];const statusLabels: Record<TaskRunStatus, string> = { dispatching: "Submitting", dispatch_error: "Submission failed", pending: "Queued", running: "Running", completed: "Completed", error: "Failed", aborted: "Cancelled", skipped: "Skipped",};const failures = { dispatch_failed: "Could not submit this run.", turn_failed: "The agent turn failed.", turn_aborted: "The agent turn was cancelled.", turn_skipped: "The agent turn was skipped.",};export function taskTime(instant: string | null) { return instant ? `${instant.slice(0, 10)} ${instant.slice(11, 19)} UTC` : "—";}export function taskScheduleLabel(task: TaskSummary) { return task.schedule.kind === "once" ? `Once · ${taskTime(task.schedule.at)}` : `${task.schedule.expression} · UTC`;}function NextRun({ task }: { task: TaskSummary }) { return task.schedulingError ? ( <span className="task-warning"> Schedule unavailable. Refresh to retry. </span> ) : ( <span> {task.nextRunAt ? taskTime(task.nextRunAt) : task.enabled ? "No upcoming run" : "None while paused"} </span> );}
export function Tasks() { const params = useParams({ strict: false }); const selected = "id" in params ? (params.id ?? null) : null; const { conversations, session: shell, creating } = useShellSession(); const cache = useCustomerQuerySession(); const [session] = useState(() => new TaskSession(cache)); const view = useSyncExternalStore( session.subscribe, session.getSnapshot, session.getServerSnapshot, ); const { connected, busy, error: commandError, notice, editor, deleting, } = view; const tasksQuery = useTaskListQuery(view.connection, busy, selected); const tasks = tasksQuery.data ?? []; const runsQuery = useTaskRunsQuery( view.connection, tasks.some((task) => task.id === selected) ? selected! : "", busy, ); const history = runsQuery.data ?? emptyTaskHistory; const reading = tasksQuery.isFetching || runsQuery.isFetching; const loading = !tasksQuery.data && !commandError && tasksQuery.isPending; const error = commandError || (tasksQuery.error || runsQuery.error ? "Could not refresh tasks. Saved details may be out of date. Your edits are still here." : ""); const portal = useRef<HTMLDivElement | null>(null); const conversationItems = useMemo( () => conversations.map((conversation) => ({ value: conversation.id, label: conversation.name, })), [conversations], ); useEffect(() => session.start(), [session]); useEffect(() => session.select(selected), [session, selected]); const task = tasks.find((item) => item.id === selected); const locked = busy || !connected; const fieldsLocked = locked || !!editor?.pending; return ( <main className="content tasks-page"> <div className="page-intro"> {selected && <Link to="/tasks">All scheduled tasks</Link>} <h1> {task?.name ?? (selected ? "Scheduled task" : "Scheduled tasks")} </h1> <p className="muted"> Work Flarebot carries out in your conversations, even when you’re away. </p> </div> <div className="task-toolbar"> <Button variant="primary" disabled={ locked || view.editorOpen || (!editor && tasks.length >= MAX_TASKS) } onClick={() => session.openEditor(undefined, conversations[0]?.id)} > {editor ? "Resume task draft" : "Create task"} </Button> <Button disabled={busy || reading} onClick={() => (connected ? session.reload() : session.connect())} loading={reading && !loading} > Refresh tasks </Button> </div> {editor && !view.editorOpen && ( <p role="status"> A task creation request is unconfirmed. Resume the draft to retry the same request. </p> )} {loading && <LoadingState label="Loading scheduled tasks…" rows={6} />} {error && !deleting && <p role="alert">{error}</p>} {notice && <p role="status">{notice}</p>} {!loading && !error && !selected && !tasks.length && ( <Empty size="sm" icon={<ClockIcon size={32} />} title={ connected ? "No scheduled tasks yet" : "Connect to view scheduled tasks" } description={ connected ? "Create a task with instructions, a schedule and a conversation for its results." : "Check your connection and sign-in to see your schedules." } /> )} {!selected && !!tasks.length && ( <ul className="task-list" aria-label="Scheduled tasks"> {tasks.map((item) => ( <li key={item.id}> <LayerCard className="task-card"> <div className="task-card-heading"> <h2> <Link to="/tasks/$id" params={{ id: item.id }}> {item.name} </Link> </h2> <span>{item.enabled ? "Enabled" : "Paused"}</span> </div> <p className="task-schedule">{taskScheduleLabel(item)}</p> <dl className="task-times"> <div> <dt>Previous run</dt> <dd> {item.previousRun ? `${statusLabels[item.previousRun.status]} · ${taskTime(item.previousRun.createdAt)}` : "No runs yet"} </dd> </div> <div> <dt>Next run</dt> <dd> <NextRun task={item} /> </dd> </div> </dl> <Link to="/tasks/$id" params={{ id: item.id }}> View task and history </Link> </LayerCard> </li> ))} </ul> )} {selected && !loading && !reading && !error && !task && ( <Empty size="sm" icon={<ClockIcon size={32} />} title={connected ? "Task not found" : "Connect to view this task"} description="It may have been deleted with its conversation. Your other tasks are available from the task list." /> )} {task && ( <div className="task-detail"> <LayerCard className="task-card"> <div className="task-card-heading"> <h2>Schedule</h2> <Switch label="Enabled" checked={task.enabled} disabled={locked || !!editor} onCheckedChange={() => void session.toggle(task)} /> </div> <p className="task-schedule">{taskScheduleLabel(task)}</p> <dl className="task-times"> <div> <dt>Previous run</dt> <dd> {task.previousRun ? `${statusLabels[task.previousRun.status]} · ${taskTime(task.previousRun.createdAt)}` : "No runs yet"} </dd> </div> <div> <dt>Next run</dt> <dd> <NextRun task={task} /> </dd> </div> </dl> <p className="muted"> Schedules use UTC. Runs can start later when Flarebot is busy. Run now leaves the schedule unchanged. </p> <div className="settings-actions"> <Button variant="primary" disabled={ locked || (!view.manualPending[task.id] && !!task.previousRun && activeRun(task.previousRun)) } onClick={() => void session.run(task)} > {view.manualPending[task.id] ? "Retry run request" : "Run now"} </Button> <Button disabled={locked || !!editor} onClick={() => session.openEditor(task)} > Edit task </Button> <Button disabled={locked || !!editor} onClick={() => session.confirmDelete(task)} > Delete task </Button> </div> </LayerCard> <section className="task-instructions" aria-labelledby="task-instructions-heading" > <h2 id="task-instructions-heading">Instructions</h2> <p>{task.instructions}</p> <Link to="/conversations/$id" params={{ id: task.conversationId }}> Open result conversation </Link> </section> <section aria-labelledby="task-history-heading" className="task-history" > <div className="task-card-heading"> <h2 id="task-history-heading">Run history</h2> {reading && <Loader />} </div> <p className="muted"> Running includes time waiting for this conversation. Results appear in its messages. </p> {reading && !history.runs.length && ( <LoadingState label="Loading run history…" /> )} {!reading && !loading && !error && !history.runs.length && ( <p>No runs yet.</p> )} <ol aria-label="Task runs"> {history.runs.map((run) => ( <li key={run.id} data-run-id={run.id} className="task-run"> <div className="task-card-heading"> <span className="task-run-status" data-status={run.status}> {statusLabels[run.status]} </span> <span> {run.source === "manual" ? "Manual" : "Scheduled"} </span> </div> <p>{taskTime(run.scheduledFor)}</p> {run.completedAt && ( <p className="muted"> Finished {taskTime(run.completedAt)} </p> )} {run.failureCode && <p>{failures[run.failureCode]}</p>} <Link to="/conversations/$id" params={{ id: run.conversationId }} > View conversation </Link> </li> ))} </ol> {history.nextCursor && ( <Button disabled={locked || reading} loading={reading} onClick={() => runsQuery.more()} > Load older runs </Button> )} </section> </div> )} <div className="task-portal" ref={portal} /> <Dialog.Root open={view.editorOpen} onOpenChange={(open) => { if (!open) session.closeEditor(); }} > <Dialog size="lg" className="task-dialog" container={portal}> <Dialog.Title> {editor?.base ? "Edit task" : "Create task"} </Dialog.Title> <Dialog.Description> Set instructions and an explicit UTC schedule. Results go to the selected conversation. </Dialog.Description> {editor && ( <form className="task-form" onSubmit={(event) => { event.preventDefault(); void session.save(); }} > <Input label="Task name" value={editor.draft.name} onValueChange={(name) => session.draft({ name })} maxLength={MAX_TASK_NAME} required disabled={fieldsLocked} /> <InputArea label="Task instructions" value={editor.draft.instructions} onValueChange={(instructions) => session.draft({ instructions }) } rows={4} maxLength={MAX_TASK_INSTRUCTIONS} description="Tell Flarebot what to do. Up to 8,000 characters." required disabled={fieldsLocked} /> {editor.base ? ( <p> Results stay in{" "} <Link to="/conversations/$id" params={{ id: editor.base.conversationId }} > this conversation </Link> . </p> ) : ( <> <Select label="Result conversation" items={conversationItems} value={editor.draft.conversationId || null} onValueChange={(conversationId) => session.draft({ conversationId: conversationId ?? "" }) } required disabled={fieldsLocked} container={portal} /> {!conversations.length && ( <div> <p>Create a conversation to hold this task’s results.</p> <Button disabled={fieldsLocked || creating} loading={creating} onClick={() => { void shell .createConversation() .then((conversation) => { if (conversation) session.setConversation( editor.id, conversation.id, ); }); }} > Create result conversation </Button> </div> )} </> )} <Select label="Schedule type" items={kinds} value={editor.draft.kind} onValueChange={(kind) => { if (kind === "once" || kind === "cron") session.draft({ kind }); }} disabled={fieldsLocked} container={portal} /> {editor.draft.kind === "once" ? ( <Input type="datetime-local" step={1} label="Date and time (UTC)" value={editor.draft.at} onValueChange={(at) => session.draft({ at })} required disabled={fieldsLocked} description="Use UTC, not your device’s local time. New one-off schedules must be in the future." /> ) : ( <> <Input label="Cron expression (UTC)" value={editor.draft.expression} onValueChange={(expression) => session.draft({ expression }) } maxLength={120} required disabled={fieldsLocked} description="Five numeric fields: minute, hour, day of month, month, weekday (0–6, Sunday–Saturday). Supports *, ranges, lists and steps." /> <div className="settings-actions"> <Button disabled={fieldsLocked} onClick={() => session.draft({ expression: "0 9 * * *" })} > Daily at 09:00 UTC </Button> <Button disabled={fieldsLocked} onClick={() => session.draft({ expression: "0 9 * * 1" })} > Mondays at 09:00 UTC </Button> </div> </> )} <Switch label="Enable scheduled runs" checked={editor.draft.enabled} disabled={fieldsLocked} onCheckedChange={(enabled) => session.draft({ enabled })} /> {editor.error && <p role="alert">{editor.error}</p>} {editor.conflict && ( <Button disabled={locked} onClick={() => void session.rebase()}> Load latest version, keep edits </Button> )} <div className="settings-actions"> <Button type="submit" variant="primary" disabled={ locked || editor.conflict || !editor.draft.conversationId } loading={busy} > {editor.pending ? "Retry save" : "Save task"} </Button> <Button disabled={busy} onClick={session.closeEditor}> {editor.pending ? "Close and keep request" : "Cancel"} </Button> </div> </form> )} </Dialog> </Dialog.Root> <Dialog.Root open={!!deleting} onOpenChange={(open) => { if (!open) session.confirmDelete(null); }} > <Dialog className="task-dialog" container={portal}> <Dialog.Title>Delete task?</Dialog.Title> <Dialog.Description> Delete “{deleting?.name}” and its run history. Future runs stop and active task work is cancelled. Messages in its conversation are kept. </Dialog.Description> {error && <p role="alert">{error}</p>} <div className="settings-actions"> <Button variant="primary" disabled={locked} loading={busy} onClick={() => void session.remove()} > Delete task permanently </Button> <Button disabled={busy} onClick={() => session.confirmDelete(null)}> Cancel </Button> </div> </Dialog> </Dialog.Root> </main> );}