diff --git a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts b/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts
index 3a582d9bd..7f44a8aea 100644
--- a/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts
+++ b/apps/webapp/app/presenters/TaskDetailsPresenter.server.ts
@@ -63,6 +63,18 @@ export class TaskDetailsPresenter {
completedAt: true,
style: true,
parentId: true,
+ attempts: {
+ select: {
+ number: true,
+ status: true,
+ error: true,
+ runAt: true,
+ updatedAt: true,
+ },
+ orderBy: {
+ number: "asc",
+ },
+ },
},
where: {
id,
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/TaskAttemptStatus.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/TaskAttemptStatus.tsx
new file mode 100644
index 000000000..67c699f28
--- /dev/null
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/TaskAttemptStatus.tsx
@@ -0,0 +1,90 @@
+import {
+ CheckCircleIcon,
+ ClockIcon,
+ XCircleIcon,
+} from "@heroicons/react/24/solid";
+import { TaskAttemptStatus } from "@trigger.dev/database";
+import { Paragraph } from "~/components/primitives/Paragraph";
+import { Spinner } from "~/components/primitives/Spinner";
+import { cn } from "~/utils/cn";
+
+type TaskAttemptStatusProps = {
+ status: TaskAttemptStatus;
+ className?: string;
+};
+
+export function TaskAttemptStatusLabel({
+ status,
+}: {
+ status: TaskAttemptStatus;
+}) {
+ return (
+
+
+
+ {taskAttemptStatusTitle(status)}
+
+
+ );
+}
+
+export function TaskAttemptStatusIcon({
+ status,
+ className,
+}: TaskAttemptStatusProps) {
+ switch (status) {
+ case "COMPLETED":
+ return (
+
+ );
+ case "PENDING":
+ return (
+
+ );
+ case "STARTED":
+ return (
+
+ );
+ case "ERRORED":
+ return (
+
+ );
+ }
+}
+
+function taskAttemptStatusClassNameColor(status: TaskAttemptStatus): string {
+ switch (status) {
+ case "COMPLETED":
+ return "text-green-500";
+ case "PENDING":
+ return "text-slate-500";
+ case "STARTED":
+ return "text-blue-500";
+ case "ERRORED":
+ return "text-rose-500";
+ }
+}
+
+function taskAttemptStatusTitle(status: TaskAttemptStatus): string {
+ switch (status) {
+ case "COMPLETED":
+ return "Complete";
+ case "PENDING":
+ return "Scheduled";
+ case "STARTED":
+ return "Running";
+ case "ERRORED":
+ return "Error";
+ }
+}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route.tsx
index 21397c7dd..6686524eb 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam.tasks.$taskParam/route.tsx
@@ -23,6 +23,15 @@ import {
} from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/RunCard";
import { TaskStatusIcon } from "../_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/TaskStatus";
import { sensitiveDataReplacer } from "~/services/logger";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHeader,
+ TableHeaderCell,
+ TableRow,
+} from "~/components/primitives/Table";
+import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -63,6 +72,7 @@ export default function Page() {
properties,
output,
style,
+ attempts,
} = task;
return (
@@ -132,6 +142,37 @@ export default function Page() {
)}
+
+ {attempts.length > 0 && (
+
+
Retries
+
+
+ Attempt
+ Status
+ Date
+ Error
+
+
+ {attempts.map((attempt) => (
+
+ {attempt.number}
+
+
+
+
+ {attempt.status === "PENDING" && attempt.runAt
+ ? formatDateTime(attempt.runAt, "long")
+ : formatDateTime(attempt.updatedAt, "long")}
+
+ {attempt.error}
+
+ ))}
+
+
+
+ )}
+
Input
{params ? (