import { useQuery } from '@tanstack/react-query';
import { Link } from '@tanstack/react-router';
import { useDeferredValue, useState } from 'react';
import { Alert, AlertDescription, AlertTitle } from '../cadence-ui/components/alert';
import { Badge } from '../cadence-ui/components/badge';
import { Button } from '../cadence-ui/components/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '../cadence-ui/components/card';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../cadence-ui/components/dialog';
import { Input } from '../cadence-ui/components/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../cadence-ui/components/tabs';
import { Textarea } from '../cadence-ui/components/textarea';
import { cn } from '../cadence-ui/lib/cn';
import {
type BlockedQueueItem,
type BlockedTask,
type Message,
type Run,
type RunDetail,
type RunListItem,
type Task,
type ThreadDetail,
getRunDetail,
getThreadDetail,
listBlockedQueue,
listRuns,
} from '../lib/api';
import {
formatDateTime,
formatJson,
hasStructuredData,
pluralize,
statusLabel,
uniqueCount,
} from '../lib/format';
const terminalStatuses = new Set(['cancelled', 'completed', 'done', 'failed']);
export function RunsPage() {
const runsQuery = useQuery({
queryKey: ['runs'],
queryFn: listRuns,
refetchInterval: 15_000,
});
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search.trim().toLowerCase());
const runs = runsQuery.data ?? [];
const filteredRuns = runs.filter((item) => matchesRun(item, deferredSearch));
const totalRuns = runs.length;
const activeRuns = runs.filter((item) => !terminalStatuses.has(item.run.status)).length;
const blockedTasks = runs.reduce((sum, item) => sum + (item.task_counts.blocked ?? 0), 0);
const totalTasks = runs.reduce((sum, item) => sum + item.total_tasks, 0);
return (
setSearch(event.target.value)}
placeholder="Filter by run id, goal, or summary"
value={search}
/>
0 ? 'Need operator attention' : 'No current blockers'}
tone={blockedTasks > 0 ? 'accent' : 'subtle'}
/>
{runsQuery.isLoading ? (
) : runsQuery.isError ? (
void runsQuery.refetch()}
title="Unable to load orchestration runs"
/>
) : filteredRuns.length === 0 ? (
) : (
{filteredRuns.map((item) => (
))}
)}
);
}
export function BlockedQueuePage() {
const blockedQuery = useQuery({
queryKey: ['blocked-queue'],
queryFn: listBlockedQueue,
refetchInterval: 10_000,
});
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search.trim().toLowerCase());
const blockedItems = blockedQuery.data ?? [];
const filteredItems = blockedItems.filter((item) => matchesBlockedItem(item, deferredSearch));
const impactedRuns = uniqueCount(blockedItems, (item) => item.run.run_id);
const waitingWorkers = uniqueCount(blockedItems, (item) => item.attempt.assigned_to);
return (
setSearch(event.target.value)}
placeholder="Filter by run, task, worker, or question"
value={search}
/>
0 ? 'Awaiting operator input' : 'Queue is clear'}
tone={blockedItems.length > 0 ? 'accent' : 'subtle'}
/>
{blockedQuery.isLoading ? (
) : blockedQuery.isError ? (
void blockedQuery.refetch()}
title="Unable to load the blocked queue"
/>
) : filteredItems.length === 0 ? (
) : (
{filteredItems.map((item) => (
))}
)}
);
}
export function RunDetailPage({ runId }: { runId: string }) {
const runQuery = useQuery({
queryKey: ['run', runId],
queryFn: () => getRunDetail(runId),
refetchInterval: 10_000,
});
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search.trim().toLowerCase());
if (runQuery.isLoading) {
return ;
}
if (runQuery.isError) {
return (
void runQuery.refetch()}
title={`Unable to load run ${runId}`}
/>
);
}
const detail = runQuery.data;
if (!detail) {
return (
void runQuery.refetch()}
title={`Missing run data for ${runId}`}
/>
);
}
const filteredTasks = detail.tasks.filter((task) => matchesTask(task, deferredSearch));
const blockedByTaskID = new Map(detail.blocked_tasks.map((item) => [item.task.task_id, item] as const));
const groupedTasks = groupTasks(filteredTasks);
const statusEntries = sortTaskCounts(detail.task_counts);
return (
0 ? 'accent' : 'default'}>
{detail.run.run_id}
{detail.total_tasks} total tasks
{detail.run.goal}
{detail.run.summary}
{statusEntries.map(([status, count]) => (
{statusLabel(status)} ยท {count}
))}
Back to runs
Global blocked queue
{detail.blocked_tasks.length > 0 ? (
}
variant="warning"
>
{detail.blocked_tasks.length} blocked {pluralize(detail.blocked_tasks.length, 'task')}
This run currently has work waiting on a reply. Open the blocked tab for the question summaries and thread links.
) : null}
Overview
Task board
Blocked
{detail.blocked_tasks.length}
setSearch(event.target.value)}
placeholder="Filter tasks by id, title, summary, or owner"
value={search}
/>
Run posture
High-level task distribution across the current run state.
0 ? 'Needs answer' : 'No blockers'}
tone={detail.blocked_tasks.length > 0 ? 'accent' : 'subtle'}
/>
Status mix
Current task counts grouped by orchestration status.
{statusEntries.map(([status, count]) => (
{statusLabel(status)}
{count}
))}
{filteredTasks.length === 0 ? (
) : (
{groupedTasks.map(([status, tasks]) => (
{statusLabel(status)}
{pluralize(tasks.length, 'task')}
{tasks.map((task) => (
))}
))}
)}
{detail.blocked_tasks.length === 0 ? (
) : (
{detail.blocked_tasks.map((item) => (
))}
)}
);
}
export function ThreadTimelinePage({ threadId }: { threadId: string }) {
const threadQuery = useQuery({
queryKey: ['thread', threadId],
queryFn: () => getThreadDetail(threadId),
refetchInterval: 5_000,
});
if (threadQuery.isLoading) {
return ;
}
if (threadQuery.isError) {
return (
void threadQuery.refetch()}
title={`Unable to load thread ${threadId}`}
/>
);
}
const detail = threadQuery.data;
if (!detail) {
return (
void threadQuery.refetch()}
title={`Missing thread data for ${threadId}`}
/>
);
}
const artifactCount = detail.messages.reduce((sum, message) => sum + message.artifacts.length, 0);
return (
{detail.thread.thread_id}
{detail.thread.subject}
Leader-to-worker timeline for task {detail.thread.task_id} in run {detail.thread.run_id}.
Back to run
Blocked queue
{pluralize(detail.messages.length, 'message')}
{pluralize(artifactCount, 'artifact')}
{detail.thread.status === 'blocked' ? (
} variant="warning">
Thread is blocked
The active worker is waiting on an answer in this thread. Review the latest question before replying through the CLI or future operator actions.
) : null}
{detail.messages.length === 0 ? (
) : (
{detail.messages.map((message, index) => (
))}
)}
);
}
function PageHero({
eyebrow,
title,
description,
children,
}: {
eyebrow: string;
title: string;
description: string;
children?: React.ReactNode;
}) {
return (
{eyebrow}
{title}
{description}
Operator filter
Narrow the current surface without leaving the page.
{children}
);
}
function MetricsGrid({ children }: { children: React.ReactNode }) {
return ;
}
function MetricCard({
label,
value,
caption,
tone,
}: {
label: string;
value: string;
caption: string;
tone: 'accent' | 'default' | 'subtle';
}) {
return (
{label}
);
}
function RunSummaryCard({ item }: { item: RunListItem }) {
const blockedCount = item.task_counts.blocked ?? 0;
return (
0 ? 'accent' : 'default'}>
{item.run.run_id}
Updated {formatDateTime(item.run.updated_at)}
{item.run.goal}
{item.run.summary}
{sortTaskCounts(item.task_counts).map(([status, count]) => (
{statusLabel(status)}
{count}
))}
Open run
Blocked queue
);
}
function TaskCard({
task,
blockedTask,
}: {
task: Task;
blockedTask?: BlockedTask;
}) {
return (
{task.title}
{task.summary}
Owner
{task.default_to || 'Unassigned'}
Latest attempt
{task.latest_attempt_no || 0}
{blockedTask ? (
Waiting on reply
{blockedTask.question.summary}
{blockedTask.question.body}
) : null}
{blockedTask ? (
Open timeline
) : null}
{hasStructuredData(task.acceptance_json) ? (
) : null}
);
}
function BlockedTaskCard({ item }: { item: BlockedQueueItem }) {
return (
{item.run.run_id}
{item.task.task_id}
{item.task.title}
{item.task.summary}
attempt {item.attempt.attempt_no}
assigned to {item.attempt.assigned_to}
{formatDateTime(item.question.created_at)}
Latest question
{item.question.summary}
{item.question.body}
Open run
Thread timeline
{hasStructuredData(item.question.payload_json) ? (
) : null}
);
}
function ThreadMessageCard({
index,
message,
total,
}: {
index: number;
message: Message;
total: number;
}) {
return (
{index < total - 1 ? (
) : null}
{message.kind}
{message.from_agent}
to
{message.to_agent}
{formatDateTime(message.created_at)}
{message.summary}
{message.message_id}
{message.body}
{hasStructuredData(message.payload_json) ? (
) : null}
{message.artifacts.length > 0 ? (
{pluralize(message.artifacts.length, 'artifact')}
) : null}
{message.artifacts.length > 0 ? (
{message.artifacts.map((artifact) => (
{artifact.path}
{artifact.kind}
{hasStructuredData(artifact.metadata_json) ? (
) : null}
))}
) : null}
);
}
function QueryErrorState({
title,
description,
onRetry,
}: {
title: string;
description: string;
onRetry: () => void;
}) {
return (
} variant="destructive">
{title}
{description}
Retry request
);
}
function EmptyState({
title,
description,
}: {
title: string;
description: string;
}) {
return (
);
}
function LoadingState({ count }: { count: number }) {
return (
{Array.from({ length: count }, (_, index) => (
))}
);
}
function CompactStat({
label,
value,
}: {
label: string;
value: string;
}) {
return (
);
}
function JsonDialog({
label,
title,
description,
value,
}: {
label: string;
title: string;
description: string;
value: unknown;
}) {
const formatted = formatJson(value);
const rows = Math.min(Math.max(formatted.split('\n').length, 8), 18);
return (
{label}
{title}
{description}
);
}
function StatusBadge({ status }: { status: string }) {
return (
{statusLabel(status)}
);
}
function PriorityBadge({ priority }: { priority: string }) {
const normalized = priority.toLowerCase();
const tone =
normalized === 'high'
? 'warning'
: normalized === 'urgent'
? 'destructive'
: normalized === 'low'
? 'neutral'
: 'primary';
return (
{priority}
);
}
function badgeToneForStatus(status: string): 'destructive' | 'neutral' | 'primary' | 'success' | 'warning' {
switch (status) {
case 'blocked':
return 'warning';
case 'done':
case 'completed':
return 'success';
case 'failed':
case 'cancelled':
return 'destructive';
case 'active':
case 'in_progress':
return 'primary';
default:
return 'neutral';
}
}
function sortTaskCounts(counts: Record) {
const order = ['blocked', 'in_progress', 'active', 'ready', 'pending', 'done', 'completed', 'failed', 'cancelled'];
const orderIndex = new Map(order.map((status, index) => [status, index] as const));
return Object.entries(counts).sort((left, right) => {
const leftOrder = orderIndex.get(left[0]) ?? Number.MAX_SAFE_INTEGER;
const rightOrder = orderIndex.get(right[0]) ?? Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return left[0].localeCompare(right[0]);
});
}
function groupTasks(tasks: Task[]) {
const grouped = new Map();
for (const task of tasks) {
const bucket = grouped.get(task.status);
if (bucket) {
bucket.push(task);
continue;
}
grouped.set(task.status, [task]);
}
return sortTaskCounts(
Object.fromEntries(Array.from(grouped.entries(), ([status, items]) => [status, items.length])),
).map(([status]) => [status, grouped.get(status) ?? []] as const);
}
function matchesRun(item: RunListItem, filter: string) {
if (!filter) {
return true;
}
return [item.run.run_id, item.run.goal, item.run.summary].some((value) =>
value.toLowerCase().includes(filter),
);
}
function matchesTask(task: Task, filter: string) {
if (!filter) {
return true;
}
return [task.task_id, task.title, task.summary, task.default_to, task.priority].some(
(value) => value?.toLowerCase().includes(filter) ?? false,
);
}
function matchesBlockedItem(item: BlockedQueueItem, filter: string) {
if (!filter) {
return true;
}
return [
item.run.run_id,
item.task.task_id,
item.task.title,
item.task.summary,
item.attempt.assigned_to,
item.question.summary,
item.question.body,
item.question.from_agent,
item.question.to_agent,
].some((value) => value.toLowerCase().includes(filter));
}
function SignalIcon() {
return (
);
}
function InboxIcon() {
return (
);
}