Files
ai-workflow-skill/apps/web/src/features/operator-console.tsx
T

1191 lines
41 KiB
TypeScript

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 (
<div className="space-y-6">
<PageHero
eyebrow="Runs overview"
title="Watch the scheduler shape work across every active run."
description="This surface is read-only for now: scan run health, task distribution, and where blocked questions are starting to accumulate."
>
<Input
aria-label="Filter runs"
className="w-full md:max-w-sm"
onChange={(event) => setSearch(event.target.value)}
placeholder="Filter by run id, goal, or summary"
value={search}
/>
</PageHero>
<MetricsGrid>
<MetricCard
label="Tracked runs"
value={String(totalRuns)}
caption={`${activeRuns} currently active`}
tone="default"
/>
<MetricCard
label="Total tasks"
value={String(totalTasks)}
caption={pluralize(totalTasks, 'task')}
tone="subtle"
/>
<MetricCard
label="Blocked tasks"
value={String(blockedTasks)}
caption={blockedTasks > 0 ? 'Need operator attention' : 'No current blockers'}
tone={blockedTasks > 0 ? 'accent' : 'subtle'}
/>
</MetricsGrid>
{runsQuery.isLoading ? (
<LoadingState count={4} />
) : runsQuery.isError ? (
<QueryErrorState
description="The runs endpoint did not return data. Make sure the operator API is running and the dev server can reach it."
onRetry={() => void runsQuery.refetch()}
title="Unable to load orchestration runs"
/>
) : filteredRuns.length === 0 ? (
<EmptyState
description={
deferredSearch
? 'No runs matched the current filter. Try a run id, goal phrase, or summary keyword.'
: 'No runs are present yet. Once the scheduler has state, this page will show it here.'
}
title={deferredSearch ? 'No matching runs' : 'No runs yet'}
/>
) : (
<div className="grid gap-4 xl:grid-cols-2">
{filteredRuns.map((item) => (
<RunSummaryCard key={item.run.run_id} item={item} />
))}
</div>
)}
</div>
);
}
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 (
<div className="space-y-6">
<PageHero
eyebrow="Blocked queue"
title="Surface every worker question before it stalls the run."
description="This queue aggregates blocked tasks across all runs using the current read-only API. It is the best place to spot response bottlenecks."
>
<Input
aria-label="Filter blocked tasks"
className="w-full md:max-w-sm"
onChange={(event) => setSearch(event.target.value)}
placeholder="Filter by run, task, worker, or question"
value={search}
/>
</PageHero>
<MetricsGrid>
<MetricCard
label="Blocked tasks"
value={String(blockedItems.length)}
caption={blockedItems.length > 0 ? 'Awaiting operator input' : 'Queue is clear'}
tone={blockedItems.length > 0 ? 'accent' : 'subtle'}
/>
<MetricCard
label="Impacted runs"
value={String(impactedRuns)}
caption={pluralize(impactedRuns, 'run')}
tone="default"
/>
<MetricCard
label="Workers waiting"
value={String(waitingWorkers)}
caption={pluralize(waitingWorkers, 'worker')}
tone="subtle"
/>
</MetricsGrid>
{blockedQuery.isLoading ? (
<LoadingState count={3} />
) : blockedQuery.isError ? (
<QueryErrorState
description="The queue view composes run summaries with per-run blocked endpoints. Check that the operator API is reachable."
onRetry={() => void blockedQuery.refetch()}
title="Unable to load the blocked queue"
/>
) : filteredItems.length === 0 ? (
<EmptyState
description={
deferredSearch
? 'No blocked tasks matched the current filter.'
: 'No blocked tasks are present. This is where waiting questions will appear once runs start blocking.'
}
title={deferredSearch ? 'No matching blocked tasks' : 'Queue is clear'}
/>
) : (
<div className="grid gap-4">
{filteredItems.map((item) => (
<BlockedTaskCard key={`${item.run.run_id}-${item.task.task_id}-${item.attempt.attempt_no}`} item={item} />
))}
</div>
)}
</div>
);
}
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 <LoadingState count={4} />;
}
if (runQuery.isError) {
return (
<QueryErrorState
description="The selected run could not be loaded. It may have been removed, or the API may not be reachable."
onRetry={() => void runQuery.refetch()}
title={`Unable to load run ${runId}`}
/>
);
}
const detail = runQuery.data;
if (!detail) {
return (
<QueryErrorState
description="The API returned an empty run detail payload."
onRetry={() => 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 (
<div className="space-y-6">
<Card tone={detail.blocked_tasks.length > 0 ? 'accent' : 'default'}>
<CardHeader className="gap-4">
<div className="flex flex-col 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 tone="primary">{detail.run.run_id}</Badge>
<StatusBadge status={detail.run.status} />
<Badge tone="neutral" variant="outline">
{detail.total_tasks} total tasks
</Badge>
</div>
<CardTitle className="type-display text-[clamp(2.2rem,4vw,4rem)] leading-[0.92] tracking-[-0.05em]">
{detail.run.goal}
</CardTitle>
<CardDescription className="max-w-3xl text-base leading-7">
{detail.run.summary}
</CardDescription>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<CompactStat label="Created" value={formatDateTime(detail.run.created_at)} />
<CompactStat label="Updated" value={formatDateTime(detail.run.updated_at)} />
<CompactStat
label="Blocked now"
value={String(detail.blocked_tasks.length)}
/>
<CompactStat label="Run id" value={detail.run.run_id} />
</div>
</div>
</CardHeader>
<CardFooter className="flex flex-wrap justify-between gap-3">
<div className="flex flex-wrap gap-2">
{statusEntries.map(([status, count]) => (
<Badge key={status} tone={badgeToneForStatus(status)} variant="outline">
{statusLabel(status)} · {count}
</Badge>
))}
</div>
<div className="flex flex-wrap gap-2">
<Button asChild size="sm" variant="ghost">
<Link to="/">Back to runs</Link>
</Button>
<Button asChild size="sm" variant="subtle">
<Link to="/blocked">Global blocked queue</Link>
</Button>
</div>
</CardFooter>
</Card>
{detail.blocked_tasks.length > 0 ? (
<Alert
icon={<SignalIcon />}
variant="warning"
>
<AlertTitle>{detail.blocked_tasks.length} blocked {pluralize(detail.blocked_tasks.length, 'task')}</AlertTitle>
<AlertDescription>
This run currently has work waiting on a reply. Open the blocked tab for the question summaries and thread links.
</AlertDescription>
</Alert>
) : null}
<Tabs defaultValue="tasks">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="tasks">Task board</TabsTrigger>
<TabsTrigger value="blocked">
Blocked
<span className="ml-1 rounded-full bg-[color-mix(in_oklch,var(--color-primary)_16%,transparent)] px-1.5 py-0.5 text-[0.68rem] text-[var(--color-primary)]">
{detail.blocked_tasks.length}
</span>
</TabsTrigger>
</TabsList>
<Input
aria-label="Filter run tasks"
className="w-full lg:max-w-sm"
onChange={(event) => setSearch(event.target.value)}
placeholder="Filter tasks by id, title, summary, or owner"
value={search}
/>
</div>
<TabsContent value="overview">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.4fr)_minmax(20rem,0.8fr)]">
<Card tone="subtle">
<CardHeader>
<CardTitle>Run posture</CardTitle>
<CardDescription>
High-level task distribution across the current run state.
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<MetricCard
label="Total tasks"
value={String(detail.total_tasks)}
caption={pluralize(detail.total_tasks, 'task')}
tone="default"
/>
<MetricCard
label="Blocked"
value={String(detail.blocked_tasks.length)}
caption={detail.blocked_tasks.length > 0 ? 'Needs answer' : 'No blockers'}
tone={detail.blocked_tasks.length > 0 ? 'accent' : 'subtle'}
/>
<MetricCard
label="Status buckets"
value={String(statusEntries.length)}
caption={pluralize(statusEntries.length, 'state')}
tone="subtle"
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status mix</CardTitle>
<CardDescription>Current task counts grouped by orchestration status.</CardDescription>
</CardHeader>
<CardContent className="grid gap-3">
{statusEntries.map(([status, count]) => (
<div
className="flex items-center justify-between rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-surface)_78%,white_22%)] px-4 py-3"
key={status}
>
<div className="flex items-center gap-3">
<StatusBadge status={status} />
<span className="text-sm text-[var(--color-foreground)]">{statusLabel(status)}</span>
</div>
<span className="text-sm font-semibold text-[var(--color-foreground)]">{count}</span>
</div>
))}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="tasks">
{filteredTasks.length === 0 ? (
<EmptyState
description={
deferredSearch
? 'No tasks matched the current filter.'
: 'This run has no tasks yet.'
}
title={deferredSearch ? 'No matching tasks' : 'No tasks in this run'}
/>
) : (
<div className="grid gap-4 xl:grid-cols-3">
{groupedTasks.map(([status, tasks]) => (
<Card key={status} tone={status === 'blocked' ? 'accent' : 'subtle'}>
<CardHeader>
<div className="flex items-center justify-between gap-3">
<div className="space-y-1">
<CardTitle>{statusLabel(status)}</CardTitle>
<CardDescription>{pluralize(tasks.length, 'task')}</CardDescription>
</div>
<StatusBadge status={status} />
</div>
</CardHeader>
<CardContent className="grid gap-3">
{tasks.map((task) => (
<TaskCard
blockedTask={blockedByTaskID.get(task.task_id)}
key={task.task_id}
task={task}
/>
))}
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
<TabsContent value="blocked">
{detail.blocked_tasks.length === 0 ? (
<EmptyState
description="No tasks in this run are currently waiting on a reply."
title="No blocked tasks"
/>
) : (
<div className="grid gap-4">
{detail.blocked_tasks.map((item) => (
<BlockedTaskCard
item={{
...item,
run: detail.run,
}}
key={`${item.task.task_id}-${item.attempt.attempt_no}`}
/>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
);
}
export function ThreadTimelinePage({ threadId }: { threadId: string }) {
const threadQuery = useQuery({
queryKey: ['thread', threadId],
queryFn: () => getThreadDetail(threadId),
refetchInterval: 5_000,
});
if (threadQuery.isLoading) {
return <LoadingState count={3} />;
}
if (threadQuery.isError) {
return (
<QueryErrorState
description="The selected thread could not be loaded. Make sure the thread id is still valid and the operator API can reach the shared SQLite database."
onRetry={() => void threadQuery.refetch()}
title={`Unable to load thread ${threadId}`}
/>
);
}
const detail = threadQuery.data;
if (!detail) {
return (
<QueryErrorState
description="The API returned an empty thread payload."
onRetry={() => void threadQuery.refetch()}
title={`Missing thread data for ${threadId}`}
/>
);
}
const artifactCount = detail.messages.reduce((sum, message) => sum + message.artifacts.length, 0);
return (
<div className="space-y-6">
<Card tone={detail.thread.status === 'blocked' ? 'accent' : 'default'}>
<CardHeader className="gap-4">
<div className="flex flex-col 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 tone="primary">{detail.thread.thread_id}</Badge>
<StatusBadge status={detail.thread.status} />
<PriorityBadge priority={detail.thread.priority} />
</div>
<CardTitle className="type-display text-[clamp(2.1rem,3.6vw,3.4rem)] leading-[0.94] tracking-[-0.05em]">
{detail.thread.subject}
</CardTitle>
<CardDescription className="max-w-3xl text-base leading-7">
Leader-to-worker timeline for task {detail.thread.task_id} in run {detail.thread.run_id}.
</CardDescription>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<CompactStat label="Created by" value={detail.thread.created_by} />
<CompactStat label="Assigned to" value={detail.thread.assigned_to} />
<CompactStat label="Opened" value={formatDateTime(detail.thread.created_at)} />
<CompactStat label="Updated" value={formatDateTime(detail.thread.updated_at)} />
</div>
</div>
</CardHeader>
<CardFooter className="flex flex-wrap justify-between gap-3">
<div className="flex flex-wrap gap-2">
<Button asChild size="sm" variant="ghost">
<Link params={{ runId: detail.thread.run_id }} to="/runs/$runId">
Back to run
</Link>
</Button>
<Button asChild size="sm" variant="subtle">
<Link to="/blocked">Blocked queue</Link>
</Button>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{pluralize(detail.messages.length, 'message')}</Badge>
<Badge variant="outline">{pluralize(artifactCount, 'artifact')}</Badge>
</div>
</CardFooter>
</Card>
{detail.thread.status === 'blocked' ? (
<Alert icon={<SignalIcon />} variant="warning">
<AlertTitle>Thread is blocked</AlertTitle>
<AlertDescription>
The active worker is waiting on an answer in this thread. Review the latest question before replying through the CLI or future operator actions.
</AlertDescription>
</Alert>
) : null}
{detail.messages.length === 0 ? (
<EmptyState
description="This thread exists, but it does not have any messages yet."
title="No timeline messages"
/>
) : (
<div className="grid gap-4">
{detail.messages.map((message, index) => (
<ThreadMessageCard
index={index}
key={message.message_id}
message={message}
total={detail.messages.length}
/>
))}
</div>
)}
</div>
);
}
function PageHero({
eyebrow,
title,
description,
children,
}: {
eyebrow: string;
title: string;
description: string;
children?: React.ReactNode;
}) {
return (
<section className="grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(18rem,0.75fr)]">
<Card tone="accent">
<CardHeader className="gap-4">
<div className="space-y-3">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.28em] text-[var(--color-primary)]">
{eyebrow}
</p>
<CardTitle className="type-display text-[clamp(2.3rem,5vw,4.6rem)] leading-[0.9] tracking-[-0.06em]">
{title}
</CardTitle>
<CardDescription className="max-w-3xl text-base leading-7">
{description}
</CardDescription>
</div>
</CardHeader>
</Card>
<Card tone="subtle">
<CardHeader>
<CardTitle>Operator filter</CardTitle>
<CardDescription>
Narrow the current surface without leaving the page.
</CardDescription>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
</section>
);
}
function MetricsGrid({ children }: { children: React.ReactNode }) {
return <section className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{children}</section>;
}
function MetricCard({
label,
value,
caption,
tone,
}: {
label: string;
value: string;
caption: string;
tone: 'accent' | 'default' | 'subtle';
}) {
return (
<Card tone={tone}>
<CardContent className="space-y-3 p-6">
<p className="text-[0.72rem] font-semibold uppercase tracking-[0.24em] text-[var(--color-muted-foreground)]">
{label}
</p>
<div className="flex items-end justify-between gap-3">
<p className="type-display text-[clamp(2rem,4vw,3.2rem)] leading-none tracking-[-0.05em] text-[var(--color-foreground)]">
{value}
</p>
<span className="text-sm leading-6 text-[var(--color-muted-foreground)]">{caption}</span>
</div>
</CardContent>
</Card>
);
}
function RunSummaryCard({ item }: { item: RunListItem }) {
const blockedCount = item.task_counts.blocked ?? 0;
return (
<Card interactive tone={blockedCount > 0 ? 'accent' : 'default'}>
<CardHeader className="gap-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<Badge tone="primary">{item.run.run_id}</Badge>
<StatusBadge status={item.run.status} />
</div>
<span className="text-xs text-[var(--color-muted-foreground)]">
Updated {formatDateTime(item.run.updated_at)}
</span>
</div>
<div className="space-y-2">
<CardTitle className="type-display text-[2rem] leading-[0.96] tracking-[-0.05em]">
{item.run.goal}
</CardTitle>
<CardDescription className="text-base leading-7">{item.run.summary}</CardDescription>
</div>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2">
<CompactStat label="Tasks" value={String(item.total_tasks)} />
<CompactStat label="Blocked" value={String(blockedCount)} />
{sortTaskCounts(item.task_counts).map(([status, count]) => (
<div
className="flex items-center justify-between rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-surface)_82%,white_18%)] px-4 py-3"
key={status}
>
<span className="text-sm text-[var(--color-muted-foreground)]">{statusLabel(status)}</span>
<span className="text-sm font-semibold text-[var(--color-foreground)]">{count}</span>
</div>
))}
</CardContent>
<CardFooter className="gap-2">
<Button asChild size="sm" variant="primary">
<Link params={{ runId: item.run.run_id }} to="/runs/$runId">
Open run
</Link>
</Button>
<Button asChild size="sm" variant="ghost">
<Link to="/blocked">Blocked queue</Link>
</Button>
</CardFooter>
</Card>
);
}
function TaskCard({
task,
blockedTask,
}: {
task: Task;
blockedTask?: BlockedTask;
}) {
return (
<article className="space-y-4 rounded-[1.25rem] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-card)_86%,white_14%)] p-4 shadow-[var(--shadow-xs)]">
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Badge tone="primary" variant="outline">
{task.task_id}
</Badge>
<StatusBadge status={task.status} />
<PriorityBadge priority={task.priority} />
</div>
<h3 className="text-base font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
{task.title}
</h3>
<p className="text-sm leading-6 text-[var(--color-muted-foreground)]">{task.summary}</p>
</div>
<dl className="grid gap-2 text-sm text-[var(--color-muted-foreground)]">
<div className="flex items-center justify-between gap-3">
<dt>Owner</dt>
<dd className="font-medium text-[var(--color-foreground)]">{task.default_to || 'Unassigned'}</dd>
</div>
<div className="flex items-center justify-between gap-3">
<dt>Latest attempt</dt>
<dd className="font-medium text-[var(--color-foreground)]">{task.latest_attempt_no || 0}</dd>
</div>
</dl>
{blockedTask ? (
<div className="rounded-[1rem] border border-[color-mix(in_oklch,var(--color-warning)_24%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-warning)_12%,var(--color-card))] p-3">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-[color-mix(in_oklch,var(--color-warning)_72%,var(--color-foreground))]">
Waiting on reply
</p>
<p className="mt-2 text-sm font-medium text-[var(--color-foreground)]">
{blockedTask.question.summary}
</p>
<p className="mt-1 text-sm leading-6 text-[var(--color-muted-foreground)]">
{blockedTask.question.body}
</p>
</div>
) : null}
<div className="flex flex-wrap gap-2">
{blockedTask ? (
<Button asChild size="sm" variant="primary">
<Link params={{ threadId: blockedTask.attempt.thread_id }} to="/threads/$threadId">
Open timeline
</Link>
</Button>
) : null}
{hasStructuredData(task.acceptance_json) ? (
<JsonDialog
description="Acceptance data is shown exactly as returned by the HTTP API."
label="Acceptance JSON"
title={`Acceptance for ${task.task_id}`}
value={task.acceptance_json}
/>
) : null}
</div>
</article>
);
}
function BlockedTaskCard({ item }: { item: BlockedQueueItem }) {
return (
<Card tone="accent">
<CardHeader className="gap-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<Badge tone="primary">{item.run.run_id}</Badge>
<Badge variant="outline">{item.task.task_id}</Badge>
<PriorityBadge priority={item.task.priority} />
</div>
<div className="space-y-2">
<CardTitle>{item.task.title}</CardTitle>
<CardDescription className="text-base leading-7">{item.task.summary}</CardDescription>
</div>
</div>
<div className="grid gap-2 text-sm text-[var(--color-muted-foreground)]">
<div className="flex items-center gap-2">
<StatusBadge status={item.task.status} />
<span>attempt {item.attempt.attempt_no}</span>
</div>
<span>assigned to {item.attempt.assigned_to}</span>
<span>{formatDateTime(item.question.created_at)}</span>
</div>
</div>
</CardHeader>
<CardContent className="grid gap-4 xl:grid-cols-[minmax(0,1.3fr)_minmax(16rem,0.7fr)]">
<div className="space-y-3 rounded-[1.25rem] border border-[color-mix(in_oklch,var(--color-warning)_26%,var(--color-border))] bg-[color-mix(in_oklch,var(--color-warning)_10%,var(--color-card))] p-4">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-[color-mix(in_oklch,var(--color-warning)_72%,var(--color-foreground))]">
Latest question
</p>
<h3 className="text-base font-semibold tracking-[var(--tracking-tight)] text-[var(--color-foreground)]">
{item.question.summary}
</h3>
<p className="whitespace-pre-wrap text-sm leading-6 text-[var(--color-muted-foreground)]">
{item.question.body}
</p>
</div>
<div className="grid gap-3 text-sm">
<CompactStat label="From" value={item.question.from_agent} />
<CompactStat label="To" value={item.question.to_agent} />
<CompactStat label="Thread" value={item.attempt.thread_id} />
</div>
</CardContent>
<CardFooter className="flex flex-wrap gap-2">
<Button asChild size="sm" variant="primary">
<Link params={{ runId: item.run.run_id }} to="/runs/$runId">
Open run
</Link>
</Button>
<Button asChild size="sm" variant="subtle">
<Link params={{ threadId: item.attempt.thread_id }} to="/threads/$threadId">
Thread timeline
</Link>
</Button>
{hasStructuredData(item.question.payload_json) ? (
<JsonDialog
description="This is the raw question payload returned by the API."
label="Question payload"
title={`Payload for ${item.task.task_id}`}
value={item.question.payload_json}
/>
) : null}
</CardFooter>
</Card>
);
}
function ThreadMessageCard({
index,
message,
total,
}: {
index: number;
message: Message;
total: number;
}) {
return (
<div className="grid gap-3 md:grid-cols-[2.5rem_minmax(0,1fr)]">
<div className="hidden md:flex md:flex-col md:items-center">
<div className="mt-3 h-3.5 w-3.5 rounded-full border-2 border-[var(--color-primary)] bg-[var(--color-card)]" />
{index < total - 1 ? (
<div className="mt-2 w-px flex-1 bg-[color-mix(in_oklch,var(--color-border)_72%,transparent)]" />
) : null}
</div>
<Card tone="subtle">
<CardHeader className="gap-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap items-center gap-2">
<Badge tone="primary" variant="outline">
{message.kind}
</Badge>
<Badge variant="outline">{message.from_agent}</Badge>
<span className="text-sm text-[var(--color-muted-foreground)]">to</span>
<Badge variant="outline">{message.to_agent}</Badge>
</div>
<span className="text-xs text-[var(--color-muted-foreground)]">
{formatDateTime(message.created_at)}
</span>
</div>
<div className="space-y-2">
<CardTitle>{message.summary}</CardTitle>
<CardDescription className="text-sm leading-6">
{message.message_id}
</CardDescription>
</div>
</CardHeader>
<CardContent className="grid gap-4">
<p className="whitespace-pre-wrap text-sm leading-7 text-[var(--color-foreground)]">
{message.body}
</p>
<div className="flex flex-wrap gap-2">
{hasStructuredData(message.payload_json) ? (
<JsonDialog
description="Raw message payload as returned by the backend."
label="Payload JSON"
title={`Payload for ${message.message_id}`}
value={message.payload_json}
/>
) : null}
{message.artifacts.length > 0 ? (
<Badge tone="warning" variant="outline">
{pluralize(message.artifacts.length, 'artifact')}
</Badge>
) : null}
</div>
{message.artifacts.length > 0 ? (
<div className="grid gap-3">
{message.artifacts.map((artifact) => (
<div
className="flex flex-col gap-3 rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-card)_90%,white_10%)] px-4 py-3 lg:flex-row lg:items-center lg:justify-between"
key={artifact.artifact_id}
>
<div className="space-y-1">
<p className="text-sm font-medium text-[var(--color-foreground)]">{artifact.path}</p>
<p className="text-xs uppercase tracking-[0.16em] text-[var(--color-muted-foreground)]">
{artifact.kind}
</p>
</div>
{hasStructuredData(artifact.metadata_json) ? (
<JsonDialog
description="Artifact metadata emitted by the worker."
label="Artifact metadata"
title={`Artifact ${artifact.artifact_id}`}
value={artifact.metadata_json}
/>
) : null}
</div>
))}
</div>
) : null}
</CardContent>
</Card>
</div>
);
}
function QueryErrorState({
title,
description,
onRetry,
}: {
title: string;
description: string;
onRetry: () => void;
}) {
return (
<Alert icon={<SignalIcon />} variant="destructive">
<AlertTitle>{title}</AlertTitle>
<AlertDescription className="flex flex-col gap-3">
<span>{description}</span>
<div>
<Button onClick={onRetry} size="sm" variant="subtle">
Retry request
</Button>
</div>
</AlertDescription>
</Alert>
);
}
function EmptyState({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<Card tone="subtle">
<CardContent className="flex min-h-64 flex-col items-center justify-center gap-4 p-10 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-[color-mix(in_oklch,var(--color-border)_76%,transparent)] bg-[color-mix(in_oklch,var(--color-card)_84%,white_16%)] text-[var(--color-primary)]">
<InboxIcon />
</div>
<div className="space-y-2">
<h2 className="type-display text-[2rem] leading-none tracking-[-0.04em] text-[var(--color-foreground)]">
{title}
</h2>
<p className="mx-auto max-w-2xl text-sm leading-7 text-[var(--color-muted-foreground)]">
{description}
</p>
</div>
</CardContent>
</Card>
);
}
function LoadingState({ count }: { count: number }) {
return (
<div className="grid gap-4 xl:grid-cols-2">
{Array.from({ length: count }, (_, index) => (
<Card key={index} tone="subtle">
<CardContent className="grid gap-4 p-6">
<div className="h-3 w-28 animate-pulse rounded-full bg-[color-mix(in_oklch,var(--color-border)_82%,white_18%)]" />
<div className="h-12 w-3/4 animate-pulse rounded-[1rem] bg-[color-mix(in_oklch,var(--color-border)_72%,white_28%)]" />
<div className="h-4 w-full animate-pulse rounded-full bg-[color-mix(in_oklch,var(--color-border)_70%,white_30%)]" />
<div className="h-4 w-5/6 animate-pulse rounded-full bg-[color-mix(in_oklch,var(--color-border)_70%,white_30%)]" />
</CardContent>
</Card>
))}
</div>
);
}
function CompactStat({
label,
value,
}: {
label: string;
value: string;
}) {
return (
<div className="rounded-[1rem] border border-[color-mix(in_oklch,var(--color-border)_72%,transparent)] bg-[color-mix(in_oklch,var(--color-surface)_82%,white_18%)] px-4 py-3">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-[var(--color-muted-foreground)]">
{label}
</p>
<p className="mt-2 break-all text-sm font-medium text-[var(--color-foreground)]">{value}</p>
</div>
);
}
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 (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="ghost">
{label}
</Button>
</DialogTrigger>
<DialogContent size="lg">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Textarea
className="type-mono text-xs"
readOnly
rows={rows}
value={formatted}
/>
</DialogContent>
</Dialog>
);
}
function StatusBadge({ status }: { status: string }) {
return (
<Badge tone={badgeToneForStatus(status)} variant="subtle">
{statusLabel(status)}
</Badge>
);
}
function PriorityBadge({ priority }: { priority: string }) {
const normalized = priority.toLowerCase();
const tone =
normalized === 'high'
? 'warning'
: normalized === 'urgent'
? 'destructive'
: normalized === 'low'
? 'neutral'
: 'primary';
return (
<Badge tone={tone} variant="outline">
{priority}
</Badge>
);
}
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<string, number>) {
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<string, Task[]>();
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 (
<svg aria-hidden="true" className="h-5 w-5" fill="none" viewBox="0 0 24 24">
<path
d="M4 12h3l2-5 4 11 2-6h5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}
function InboxIcon() {
return (
<svg aria-hidden="true" className="h-6 w-6" fill="none" viewBox="0 0 24 24">
<path
d="M4 6.5h16v8.8l-2.4 2.7H6.4L4 15.3V6.5Z"
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="1.8"
/>
<path
d="M4.8 14.5h4.5l1.5 2h2.4l1.5-2h4.5"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.8"
/>
</svg>
);
}