337 lines
15 KiB
TypeScript
337 lines
15 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { listFleetAgents } from "@/lib/agents";
|
|
import { findTask, listTaskEvents } from "@/lib/tasks";
|
|
import { formatDateTime } from "@/lib/utils";
|
|
import type { FleetAgent, TaskEvent, TaskRecord } from "@/lib/types";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
function familyVariant(family: TaskRecord["family"]) {
|
|
if (family === "zeroclaw") {
|
|
return "success";
|
|
}
|
|
if (family === "direct") {
|
|
return "warning";
|
|
}
|
|
return "default";
|
|
}
|
|
|
|
function dispatchVariant(state: TaskRecord["dispatch_state"]) {
|
|
return state === "failed" ? "warning" : state === "completed" ? "success" : "secondary";
|
|
}
|
|
|
|
function collectDependencyRows(task: TaskRecord) {
|
|
const rows = [
|
|
{ label: "Template", value: task.template_key },
|
|
{ label: "Repository", value: task.repo_slug },
|
|
{ label: "Base Branch", value: task.base_branch },
|
|
{ label: "Preferred Agent", value: task.preferred_agent },
|
|
{ label: "Model Hint", value: task.model_hint },
|
|
{ label: "Reasoning Effort", value: task.reasoning_effort },
|
|
];
|
|
|
|
return rows.filter((row) => row.value);
|
|
}
|
|
|
|
function collectRequirementRows(task: TaskRecord) {
|
|
const rows = [
|
|
{ label: "Assignee", value: task.assignee || "Unassigned" },
|
|
{ label: "Family", value: task.family || "manual" },
|
|
{ label: "Target Host", value: task.target_host || "n/a" },
|
|
{ label: "Target Channel", value: task.target_channel || "n/a" },
|
|
{ label: "Dispatch Method", value: task.dispatch_method },
|
|
{ label: "Dispatch State", value: task.dispatch_state },
|
|
{ label: "Priority", value: task.priority },
|
|
{ label: "Status", value: task.status },
|
|
];
|
|
|
|
return rows;
|
|
}
|
|
|
|
function findAssignedAgent(task: TaskRecord, agents: FleetAgent[]) {
|
|
return agents.find(
|
|
(agent) =>
|
|
agent.assignmentKey === task.assignee ||
|
|
agent.slug === task.assignee ||
|
|
agent.aliases.includes(task.assignee),
|
|
);
|
|
}
|
|
|
|
function renderEventTone(event: TaskEvent["event_type"]) {
|
|
if (event === "dispatch_failed") {
|
|
return "border-amber-400/20 bg-amber-500/5";
|
|
}
|
|
if (event === "dispatch_succeeded" || event === "acknowledged") {
|
|
return "border-emerald-400/20 bg-emerald-500/5";
|
|
}
|
|
return "border-white/10 bg-slate-950/40";
|
|
}
|
|
|
|
export default async function TaskDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = await params;
|
|
const numericId = Number(id);
|
|
if (!Number.isInteger(numericId) || numericId <= 0) {
|
|
notFound();
|
|
}
|
|
|
|
const [task, events, agents] = await Promise.all([
|
|
findTask(numericId),
|
|
listTaskEvents(numericId, 100),
|
|
listFleetAgents(),
|
|
]);
|
|
|
|
if (!task) {
|
|
notFound();
|
|
}
|
|
|
|
const requirementRows = collectRequirementRows(task);
|
|
const dependencyRows = collectDependencyRows(task);
|
|
const assignedAgent = findAssignedAgent(task, agents);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader className="gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
<div className="space-y-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Badge variant="outline">Task #{task.id}</Badge>
|
|
<Badge variant={familyVariant(task.family)}>{task.family || "manual"}</Badge>
|
|
<Badge variant={dispatchVariant(task.dispatch_state)}>{task.dispatch_state}</Badge>
|
|
<Badge variant={task.priority === "Critical" ? "warning" : "outline"}>{task.priority}</Badge>
|
|
<Badge variant="secondary">{task.status}</Badge>
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-2xl text-white">{task.title}</CardTitle>
|
|
<CardDescription className="mt-2 max-w-3xl text-sm leading-6 text-slate-300">
|
|
{task.description || "No description was captured for this task."}
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
<Link
|
|
className="inline-flex h-10 items-center justify-center rounded-md border border-border bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-secondary/40"
|
|
href="/tasks"
|
|
>
|
|
Back to Board
|
|
</Link>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
<div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
|
|
<div className="space-y-6">
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Work Done</CardTitle>
|
|
<CardDescription>Latest execution output, result summary, and completion metadata.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-400">Latest Result</p>
|
|
<p className="mt-2 text-sm text-slate-100">{task.result_summary || "No result has been posted yet."}</p>
|
|
</div>
|
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-400">Completion</p>
|
|
<dl className="mt-2 space-y-2 text-sm text-slate-200">
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Completed By</dt>
|
|
<dd className="text-right">{task.completed_by || "Pending"}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Completed At</dt>
|
|
<dd className="text-right">{formatDateTime(task.completed_at)}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Acknowledged</dt>
|
|
<dd className="text-right">{formatDateTime(task.acknowledged_at)}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-400">Execution Detail</p>
|
|
<p className="mt-2 whitespace-pre-wrap break-words text-sm leading-6 text-slate-200">
|
|
{task.result_detail || task.last_error || "No detailed execution transcript has been recorded yet."}
|
|
</p>
|
|
</div>
|
|
|
|
{task.last_error ? (
|
|
<div className="rounded-xl border border-amber-400/20 bg-amber-500/5 p-4">
|
|
<p className="text-xs uppercase tracking-[0.2em] text-amber-300/80">Latest Failure</p>
|
|
<p className="mt-2 whitespace-pre-wrap break-words text-sm leading-6 text-slate-200">{task.last_error}</p>
|
|
</div>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Task History</CardTitle>
|
|
<CardDescription>Chronological events for creation, dispatch, acknowledgement, retries, and completion.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{events.length > 0 ? (
|
|
events.map((event) => (
|
|
<div className={`rounded-xl border p-4 ${renderEventTone(event.event_type)}`} key={event.id}>
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-medium capitalize text-white">{event.summary}</p>
|
|
<p className="mt-1 text-xs uppercase tracking-[0.18em] text-slate-400">
|
|
{event.event_type.replace(/_/g, " ")}
|
|
{event.state ? ` • ${event.state}` : ""}
|
|
</p>
|
|
</div>
|
|
<p className="text-xs text-slate-400">{formatDateTime(event.created_at)}</p>
|
|
</div>
|
|
{event.detail ? (
|
|
<p className="mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-slate-300">{event.detail}</p>
|
|
) : null}
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="rounded-xl border border-dashed border-white/10 bg-slate-950/30 p-6 text-sm text-slate-400">
|
|
No event history has been recorded for this task yet.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Requirements</CardTitle>
|
|
<CardDescription>Assignment, routing, and execution constraints that define this task.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<dl className="space-y-3 text-sm">
|
|
{requirementRows.map((row) => (
|
|
<div className="flex items-start justify-between gap-4 border-b border-white/5 pb-3 last:border-b-0 last:pb-0" key={row.label}>
|
|
<dt className="text-slate-400">{row.label}</dt>
|
|
<dd className="max-w-[60%] break-words text-right text-slate-100">{row.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Dependencies</CardTitle>
|
|
<CardDescription>Tracked repo context, templates, and execution hints tied to this task.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{dependencyRows.length > 0 ? (
|
|
<dl className="space-y-3 text-sm">
|
|
{dependencyRows.map((row) => (
|
|
<div className="flex items-start justify-between gap-4 border-b border-white/5 pb-3 last:border-b-0 last:pb-0" key={row.label}>
|
|
<dt className="text-slate-400">{row.label}</dt>
|
|
<dd className="max-w-[60%] break-words text-right text-slate-100">{row.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
) : (
|
|
<p className="text-sm text-slate-400">No explicit dependencies or repository context were captured for this task.</p>
|
|
)}
|
|
|
|
<div>
|
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-400">Tags</p>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{task.tags.length > 0 ? (
|
|
task.tags.map((tag) => (
|
|
<Badge key={tag} variant="outline">
|
|
{tag}
|
|
</Badge>
|
|
))
|
|
) : (
|
|
<span className="text-sm text-slate-400">No tags</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Assigned Agent</CardTitle>
|
|
<CardDescription>Resolved fleet agent context for the current assignee.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{assignedAgent ? (
|
|
<>
|
|
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="text-base font-medium text-white">{assignedAgent.name}</p>
|
|
<p className="mt-1 text-sm text-slate-400">{assignedAgent.role}</p>
|
|
</div>
|
|
<Badge variant={familyVariant(assignedAgent.family)}>{assignedAgent.family}</Badge>
|
|
</div>
|
|
<dl className="mt-4 space-y-2 text-sm text-slate-200">
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Host</dt>
|
|
<dd>{assignedAgent.host}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Runtime</dt>
|
|
<dd className="max-w-[65%] break-words text-right">{assignedAgent.runtimePath}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-3">
|
|
<dt className="text-slate-400">Last Heartbeat</dt>
|
|
<dd>{formatDateTime(assignedAgent.heartbeatAt)}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
<Link
|
|
className="inline-flex h-10 w-full items-center justify-center rounded-md border border-border bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-secondary/40"
|
|
href={`/agents/${assignedAgent.slug}`}
|
|
>
|
|
Open Agent Details
|
|
</Link>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-slate-400">No configured fleet agent matched this task assignee.</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-white/10 bg-slate-950/35">
|
|
<CardHeader>
|
|
<CardTitle>Timeline</CardTitle>
|
|
<CardDescription>Primary timestamps for audit and review.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<dl className="space-y-3 text-sm">
|
|
<div className="flex justify-between gap-4 border-b border-white/5 pb-3">
|
|
<dt className="text-slate-400">Created</dt>
|
|
<dd className="text-right text-slate-100">{formatDateTime(task.created_at)}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-4 border-b border-white/5 pb-3">
|
|
<dt className="text-slate-400">Updated</dt>
|
|
<dd className="text-right text-slate-100">{formatDateTime(task.updated_at)}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-4 border-b border-white/5 pb-3">
|
|
<dt className="text-slate-400">Last Dispatch</dt>
|
|
<dd className="text-right text-slate-100">{formatDateTime(task.last_dispatch_at)}</dd>
|
|
</div>
|
|
<div className="flex justify-between gap-4">
|
|
<dt className="text-slate-400">Completed</dt>
|
|
<dd className="text-right text-slate-100">{formatDateTime(task.completed_at)}</dd>
|
|
</div>
|
|
</dl>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|