"use client";
import {
ChevronDownIcon,
ChevronUpIcon,
CircleCheckIcon,
CircleStopIcon,
Clock3Icon,
ListChecksIcon,
LoaderCircleIcon,
MessageCircleQuestionIcon,
TriangleAlertIcon,
} from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
isActiveBackgroundTask,
type BackgroundTask,
type BackgroundTaskDetail,
type BackgroundTaskStatus,
useBackgroundTask,
useBackgroundTasks,
useCancelBackgroundTask,
} from "@/core/background-tasks";
import { useMcpTasksEnabled } from "@/core/features";
import { useI18n } from "@/core/i18n/hooks";
import { formatTimeAgo } from "@/core/utils/datetime";
import { cn } from "@/lib/utils";
export function ThreadBackgroundTasks({ threadId }: { threadId: string }) {
const { t } = useI18n();
const { enabled: mcpTasksEnabled } = useMcpTasksEnabled();
const tasksQuery = useBackgroundTasks(threadId, {
enabled: mcpTasksEnabled,
});
const cancelTask = useCancelBackgroundTask(threadId);
const tasks = tasksQuery.data ?? [];
const activeTasks = tasks.filter(isActiveBackgroundTask);
const recentTasks = tasks.filter((task) => !isActiveBackgroundTask(task));
if (!mcpTasksEnabled) {
return null;
}
return (
{t.backgroundTasks.title}
{t.backgroundTasks.description}
{tasksQuery.isLoading ? (
{t.common.loading}
) : tasksQuery.isError ? (
{t.backgroundTasks.loadFailed}
{tasksQuery.error.message}
) : tasks.length === 0 ? (
{t.backgroundTasks.empty}
{t.backgroundTasks.emptyHint}
) : (
{activeTasks.length > 0 && (
cancelTask.mutate(taskId)}
/>
)}
{recentTasks.length > 0 && (
)}
)}
);
}
function TaskSection({
threadId,
title,
tasks,
cancellingTaskId,
onCancel,
}: {
threadId: string;
title: string;
tasks: BackgroundTask[];
cancellingTaskId?: string;
onCancel?: (taskId: string) => void;
}) {
return (
{title}
{tasks.map((task) => (
))}
);
}
function BackgroundTaskCard({
threadId,
task,
isCancelling,
onCancel,
}: {
threadId: string;
task: BackgroundTask;
isCancelling: boolean;
onCancel?: (taskId: string) => void;
}) {
const { t } = useI18n();
const [detailsOpen, setDetailsOpen] = useState(false);
const detailsQuery = useBackgroundTask(threadId, task.task_id, {
enabled: detailsOpen,
});
const active = isActiveBackgroundTask(task);
const cancelling = active && (task.cancel_requested || isCancelling);
const status = taskStatusPresentation(task.status, t.backgroundTasks.status);
const canShowDetails =
task.cancel_requested ||
(task.status !== "submitted" &&
(task.status !== "working" || task.tracking_degraded));
return (
{task.task_name}
{t.backgroundTasks.created(formatTimeAgo(task.created_at))}
{t.backgroundTasks.updated(formatTimeAgo(task.updated_at))}
{cancelling ? t.backgroundTasks.cancelling : status.label}
{task.tracking_degraded && (
{t.backgroundTasks.trackingDegraded}
)}
{task.error && (
{task.error}
)}
{(canShowDetails || (active && onCancel)) && (
{canShowDetails ? (
) : (
)}
{active && onCancel && (
)}
)}
{detailsOpen && (
void detailsQuery.refetch()}
/>
)}
);
}
function BackgroundTaskDetails({
task,
isLoading,
error,
onRetry,
}: {
task: BackgroundTaskDetail | undefined;
isLoading: boolean;
error: Error | null;
onRetry: () => void;
}) {
const { t } = useI18n();
if (isLoading) {
return (
{t.common.loading}
);
}
if (error) {
return (
{t.backgroundTasks.detailsFailed}
{error.message}
);
}
if (!task) return null;
return (
{task.last_cancel_error && (
{t.backgroundTasks.cancellationRetrying(
task.cancel_attempt_count,
)}
{task.last_cancel_error}
)}
{task.notification_error && (
{task.notification_status === "dead_letter"
? t.backgroundTasks.notificationStopped
: t.backgroundTasks.notificationRetrying(
task.notification_attempt_count,
)}
{task.notification_error}
)}
{task.input_required != null && (
{t.backgroundTasks.inputUnavailable}
)}
);
}
function TaskDetailField({ label, value }: { label: string; value: unknown }) {
const formatted = formatTaskDetailValue(value);
if (formatted === null) return null;
return (
);
}
function formatTaskDetailValue(value: unknown): string | null {
if (value === null || value === undefined) return null;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
try {
return JSON.stringify(value, null, 2) ?? null;
} catch {
return null;
}
}
type StatusTranslations = {
submitted: string;
working: string;
inputRequired: string;
completed: string;
failed: string;
cancelled: string;
};
function taskStatusPresentation(
status: BackgroundTaskStatus,
labels: StatusTranslations,
) {
switch (status) {
case "submitted":
return {
Icon: Clock3Icon,
label: labels.submitted,
className: "text-blue-700 dark:text-blue-300",
spinning: false,
};
case "working":
return {
Icon: LoaderCircleIcon,
label: labels.working,
className: "text-blue-700 dark:text-blue-300",
spinning: true,
};
case "input_required":
return {
Icon: MessageCircleQuestionIcon,
label: labels.inputRequired,
className: "text-amber-700 dark:text-amber-300",
spinning: false,
};
case "completed":
return {
Icon: CircleCheckIcon,
label: labels.completed,
className: "text-emerald-700 dark:text-emerald-300",
spinning: false,
};
case "failed":
return {
Icon: TriangleAlertIcon,
label: labels.failed,
className: "text-destructive",
spinning: false,
};
case "cancelled":
return {
Icon: CircleStopIcon,
label: labels.cancelled,
className: "text-muted-foreground",
spinning: false,
};
}
}