g1t/apps/web/app/routes/repo/agents.tsx

95 lines3,737 bytesCodeBlame
1import { env } from "cloudflare:workers";
2import { Bot } from "lucide-react";
3import { Link } from "react-router";
4
5import type { Route } from "./+types/agents";
6import { page } from "../../lib/meta";
7import { Idle, RunCard, splitRuns, useLiveRefresh } from "../../components/agents";
8import { AgentInstructions } from "../../components/agent-instructions";
9import { agents } from "../../lib/services.server";
10import { getViewer, unwrap } from "../../lib/session.server";
11import { accessTo } from "../../lib/access.server";
12
13export function meta({ params, ...args }: Route.MetaArgs) {
14 return page(args, { title: `Agents at work · ${params.owner}/${params.repo} · g1t` });
15}
16
17export async function loader({ params, context }: Route.LoaderArgs) {
18 const viewer = getViewer(context);
19 const repo = { namespace: params.owner, name: params.repo };
20 const [runs, instructions] = await Promise.all([
21 agents.listRuns(viewer, { repo, limit: 60 }),
22 // What every run here reads; never holds up the page.
23 env.RUNNER.instructions(viewer, repo).catch(() => null),
24 ]);
25 return {
26 runs: unwrap(runs),
27 member: (await accessTo(context, params)).can.run,
28 instructions: instructions?.ok ? instructions.value : null,
29 };
30}
31
32export default function AgentsAtWork({ loaderData, params }: Route.ComponentProps) {
33 const { runs, member, instructions } = loaderData;
34 const { live, done } = splitRuns(runs);
35 useLiveRefresh(live.length > 0);
36 const base = `/${params.owner}/${params.repo}`;
37 return (
38 <div className="max-w-4xl">
39 <h2 className="flex items-center gap-2 text-xl font-semibold tracking-tight">
40 <Bot size={19} className="text-merged" />
41 At work
42 </h2>
43 <p className="mt-1.5 max-w-2xl text-sm text-muted">
44 Every agent g1t has running on this project, and what each is doing this minute. Each run
45 works on one pull request in a sandbox of its own: making the change, revising it, reviewing,
46 catching up, answering another agent, or planning. Checks and merge queue sandboxes show here
47 too.{member && " Message a run to steer it, or stop it and take over."}
48 </p>
49
50 <section className="mt-8">
51 <div className="flex items-baseline justify-between">
52 <h3 className="text-sm font-medium">Now</h3>
53 <span className="text-xs text-muted">
54 {live.length === 0 ? "Nothing running" : `${live.length} running`}
55 </span>
56 </div>
57 {live.length === 0 ? (
58 <div className="mt-3">
59 <Idle>
60 No agent is at work here right now. Assign an issue to g1t from the{" "}
61 <Link to={`${base}/issues`} className="text-fg underline-offset-2 hover:underline">
62 issues
63 </Link>{" "}
64 to start one.
65 </Idle>
66 </div>
67 ) : (
68 <ul className="mt-3 space-y-3">
69 {live.map((run) => (
70 <RunCard key={run.id} run={run} member={member} />
71 ))}
72 </ul>
73 )}
74 </section>
75
76 {done.length > 0 && (
77 <section className="mt-10">
78 <div className="flex items-baseline justify-between">
79 <h3 className="text-sm font-medium">Recently finished</h3>
80 <Link to={`${base}/sessions`} className="text-xs text-muted hover:text-fg">
81 All sessions
82 </Link>
83 </div>
84 <ul className="mt-3 space-y-3">
85 {done.slice(0, 20).map((run) => (
86 <RunCard key={run.id} run={run} member={member} />
87 ))}
88 </ul>
89 </section>
90 )}
91
92 {instructions && <AgentInstructions instructions={instructions} base={base} />}
93 </div>
94 );
95}