Skip to content

Commit b7c28c8

Browse files
committed
Add project run explorer pages
1 parent f5387aa commit b7c28c8

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

web/src/app/projects/[id]/page.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ export default function ProjectDetailPage() {
124124
>
125125
🧵 Sessions
126126
</Link>
127+
<Link
128+
href={`/projects/${projectId}/runs`}
129+
className="text-sm text-gray-500 hover:text-gray-700 border border-gray-300 rounded-md px-3 py-1.5 hover:bg-gray-50 transition-colors"
130+
>
131+
🔎 Runs
132+
</Link>
127133
<Link
128134
href={`/projects/${projectId}/prompts`}
129135
className="text-sm text-gray-500 hover:text-gray-700 border border-gray-300 rounded-md px-3 py-1.5 hover:bg-gray-50 transition-colors"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useParams } from "next/navigation";
5+
import { useQuery } from "@tanstack/react-query";
6+
import { RunDetailPanel } from "@/components/run-detail";
7+
import { projectsApi } from "@/lib/api";
8+
9+
export default function ProjectRunDetailPage() {
10+
const params = useParams();
11+
const projectId = params.id as string;
12+
const runId = params.runId as string;
13+
14+
const { data: run, isPending, error } = useQuery({
15+
queryKey: ["project-run", projectId, runId],
16+
queryFn: () => projectsApi.getRun(projectId, runId),
17+
});
18+
19+
if (isPending) {
20+
return (
21+
<div className="flex min-h-64 items-center justify-center">
22+
<div className="text-gray-500">Loading run…</div>
23+
</div>
24+
);
25+
}
26+
27+
if (error || !run) {
28+
return (
29+
<div className="flex min-h-64 items-center justify-center">
30+
<div className="text-red-600">Failed to load run.</div>
31+
</div>
32+
);
33+
}
34+
35+
return (
36+
<div className="mx-auto max-w-6xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
37+
<div>
38+
<Link
39+
href={`/projects/${projectId}/runs`}
40+
className="mb-2 inline-block text-sm text-gray-500 hover:text-gray-700"
41+
>
42+
← Runs
43+
</Link>
44+
<h1 className="text-2xl font-bold text-gray-900">Run Detail</h1>
45+
<p className="mt-1 text-sm text-gray-500">
46+
{run.agent_id} · {run.started_at ? new Date(run.started_at).toLocaleString() : run.run_id}
47+
</p>
48+
</div>
49+
50+
<RunDetailPanel
51+
run={run}
52+
selectedRunId={run.run_id}
53+
onSelectRun={() => {}}
54+
runIds={[run.run_id]}
55+
/>
56+
</div>
57+
);
58+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import { useParams } from "next/navigation";
5+
import { useState } from "react";
6+
import { useQuery } from "@tanstack/react-query";
7+
import { projectsApi } from "@/lib/api";
8+
9+
export default function ProjectRunsPage() {
10+
const params = useParams();
11+
const projectId = params.id as string;
12+
const [agentId, setAgentId] = useState("");
13+
const [status, setStatus] = useState("");
14+
15+
const { data: project, isPending: projectPending } = useQuery({
16+
queryKey: ["project", projectId],
17+
queryFn: () => projectsApi.get(projectId),
18+
});
19+
20+
const { data: runs, isPending, error } = useQuery({
21+
queryKey: ["project-runs", projectId, agentId, status],
22+
queryFn: () =>
23+
projectsApi.listRuns(projectId, {
24+
agentId: agentId || undefined,
25+
status: status || undefined,
26+
limit: 100,
27+
}),
28+
});
29+
30+
if (projectPending || isPending) {
31+
return (
32+
<div className="flex min-h-64 items-center justify-center">
33+
<div className="text-gray-500">Loading runs…</div>
34+
</div>
35+
);
36+
}
37+
38+
if (error || !project) {
39+
return (
40+
<div className="flex min-h-64 items-center justify-center">
41+
<div className="text-red-600">Failed to load runs.</div>
42+
</div>
43+
);
44+
}
45+
46+
const agentOptions = project.agents.map((agent) => ({
47+
id: agent.agent_id,
48+
label: `${agent.agent_type} · ${agent.agent_id}`,
49+
}));
50+
51+
return (
52+
<div className="mx-auto max-w-6xl space-y-6 px-4 py-8 sm:px-6 lg:px-8">
53+
<div>
54+
<Link
55+
href={`/projects/${projectId}`}
56+
className="mb-2 inline-block text-sm text-gray-500 hover:text-gray-700"
57+
>
58+
← Project
59+
</Link>
60+
<h1 className="text-2xl font-bold text-gray-900">Runs</h1>
61+
<p className="mt-1 text-sm text-gray-500">
62+
Runtime executions for {project.project_name}, with prompt, tool, and trace visibility.
63+
</p>
64+
</div>
65+
66+
<section className="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm">
67+
<div className="grid gap-3 md:grid-cols-2">
68+
<label className="space-y-1">
69+
<span className="text-xs font-medium uppercase tracking-wide text-gray-500">Agent</span>
70+
<select
71+
value={agentId}
72+
onChange={(event) => setAgentId(event.target.value)}
73+
className="w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 outline-none ring-0 focus:border-gray-500"
74+
>
75+
<option value="">All agents</option>
76+
{agentOptions.map((agent) => (
77+
<option key={agent.id} value={agent.id}>
78+
{agent.label}
79+
</option>
80+
))}
81+
</select>
82+
</label>
83+
84+
<label className="space-y-1">
85+
<span className="text-xs font-medium uppercase tracking-wide text-gray-500">Status</span>
86+
<select
87+
value={status}
88+
onChange={(event) => setStatus(event.target.value)}
89+
className="w-full rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-800 outline-none ring-0 focus:border-gray-500"
90+
>
91+
<option value="">All statuses</option>
92+
<option value="completed">Completed</option>
93+
<option value="failed">Failed</option>
94+
<option value="running">Running</option>
95+
</select>
96+
</label>
97+
</div>
98+
</section>
99+
100+
{!runs || runs.length === 0 ? (
101+
<div className="rounded-2xl border border-dashed border-gray-300 bg-white p-12 text-center">
102+
<div className="text-4xl">🔎</div>
103+
<p className="mt-3 font-medium text-gray-700">No runs found</p>
104+
<p className="mt-1 text-sm text-gray-400">
105+
Agent runs will appear here after Keeper or Scout activity.
106+
</p>
107+
</div>
108+
) : (
109+
<div className="space-y-3">
110+
{runs.map((run) => (
111+
<Link
112+
key={run.run_id}
113+
href={`/projects/${projectId}/runs/${run.run_id}`}
114+
className="block rounded-2xl border border-gray-200 bg-white p-4 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50"
115+
>
116+
<div className="flex flex-wrap items-start justify-between gap-4">
117+
<div className="space-y-2">
118+
<div className="flex flex-wrap items-center gap-2">
119+
<RunStatusBadge status={run.status} />
120+
<span className="rounded-full bg-slate-200 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-700">
121+
{run.role}
122+
</span>
123+
<span className="font-medium text-gray-900">{run.agent_id}</span>
124+
</div>
125+
<div className="text-sm text-gray-600">
126+
{run.invocation_source} · {run.sender_name ?? "Unknown sender"}
127+
</div>
128+
<div className="font-mono text-[11px] text-gray-400">
129+
run {run.run_id}
130+
</div>
131+
</div>
132+
133+
<div className="min-w-52 space-y-2 text-right">
134+
<div className="text-sm font-medium text-gray-900">
135+
{run.total_input_tokens + run.total_output_tokens} total tokens
136+
</div>
137+
<div className="text-xs text-gray-500">
138+
{run.llm_calls} LLM call{run.llm_calls === 1 ? "" : "s"} · {run.tool_calls} tool call{run.tool_calls === 1 ? "" : "s"}
139+
</div>
140+
<div className="text-xs text-gray-500">
141+
Started {new Date(run.started_at).toLocaleString()}
142+
</div>
143+
{run.trace_id && (
144+
<div className="font-mono text-[11px] text-gray-400">
145+
trace {run.trace_id}
146+
</div>
147+
)}
148+
</div>
149+
</div>
150+
</Link>
151+
))}
152+
</div>
153+
)}
154+
</div>
155+
);
156+
}
157+
158+
function RunStatusBadge({ status }: { status: string }) {
159+
const styles: Record<string, string> = {
160+
completed: "bg-emerald-100 text-emerald-800",
161+
failed: "bg-red-100 text-red-700",
162+
running: "bg-amber-100 text-amber-800",
163+
};
164+
return (
165+
<span className={`rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${styles[status] ?? "bg-gray-100 text-gray-700"}`}>
166+
{status}
167+
</span>
168+
);
169+
}

0 commit comments

Comments
 (0)