flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

137 lines5,801 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents as a team: lifecycle, merge queue, billing and a new shell1import { env } from "cloudflare:workers";
2import { Sparkles } from "lucide-react";
3import { Form, Link, data, redirect, useNavigation } from "react-router";
4
5import type { Plan } from "@g1t/contracts";
6
7import type { Route } from "./+types/plans";
8import { Button, EmptyState, ErrorText, Textarea, TimeAgo } from "../../components/ui";
9import { work } from "../../lib/services.server";
10import {
11 assertSameOrigin,
12 getViewer,
13 requireUser,
14 roleIn,
15 unwrap,
16} from "../../lib/session.server";
17
18export function meta({ params }: Route.MetaArgs) {
19 return [{ title: `Plan · ${params.owner}/${params.repo} · g1t` }];
20}
21
22export async function loader({ params, context }: Route.LoaderArgs) {
23 const viewer = getViewer(context);
24 // Members only; to anyone else the page does not exist.
25 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
26 const path = { namespace: params.owner, name: params.repo };
27 return { plans: unwrap(await work.listPlans(path, viewer)) };
28}
29
30export async function action({ request, params, context }: Route.ActionArgs) {
31 assertSameOrigin(request);
32 const user = requireUser(context, request);
33 const form = await request.formData();
34 const path = { namespace: params.owner, name: params.repo };
35 const started = await env.RUNNER.plan(user, path, String(form.get("brief") ?? ""));
36 if (!started.ok) return { error: started.error.message };
37 throw redirect(`/${params.owner}/${params.repo}/plans/${started.value.planId}`);
38}
39
40const STATUS: Record<Plan["status"], string> = {
41 planning: "Being written",
42 ready: "Ready to read",
43 failed: "Could not be written",
44 applied: "Applied",
45};
46
47export default function Plans({ loaderData, actionData, params }: Route.ComponentProps) {
48 const { plans } = loaderData;
49 const base = `/${params.owner}/${params.repo}`;
50 const starting = useNavigation().state === "submitting";
51 return (
52 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
53 <div className="min-w-0">
54 <h2 className="text-xl font-semibold tracking-tight">Plan work</h2>
55 <p className="mt-1 max-w-2xl text-sm text-muted">
56 Say what you want to be true when the work is done. An agent reads this
57 repository and proposes the issues that would get there, with what each
58 must pass and which have to land before which. You read it, change what
59 you like, and put g1t agents on it with one click.
60 </p>
61 <Form method="post" className="mt-5 space-y-3">
62 <Textarea
63 name="brief"
64 rows={7}
65 required
66 placeholder={
67 "The outcome, in your own words. For example:\n\nThe greeter should support a --lang flag for Spanish and French, a --shout flag that upper-cases the greeting, and a --version flag. Each should be documented in the README and covered by tests."
68 }
69 />
70 <div className="flex flex-wrap items-center gap-3">
71 <Button variant="accent" type="submit" disabled={starting}>
72 <Sparkles size={15} />
73 {starting ? "Starting the planner…" : "Plan it"}
74 </Button>
75 <span className="text-xs text-muted">
76 Nothing is opened until you have read the plan.
77 </span>
78 </div>
79 <ErrorText>{actionData?.error}</ErrorText>
80 </Form>
81
82 <h3 className="mt-10 text-sm font-medium text-muted">Plans</h3>
83 <div className="mt-3">
84 {plans.length === 0 ? (
85 <EmptyState title="No plans yet" />
86 ) : (
87 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
88 {plans.map((plan) => (
89 <li key={plan.id}>
90 <Link
91 to={`${base}/plans/${plan.id}`}
92 className="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-surface"
93 >
94 <span className="min-w-0 grow">
95 <span className="block truncate font-medium">{plan.brief}</span>
96 <span className="mt-0.5 block text-xs text-faint">
97 {plan.author.username} · <TimeAgo at={plan.createdAt} />
98 {plan.issues.length > 0 &&
99 ` · ${plan.issues.length} ${plan.issues.length === 1 ? "issue" : "issues"}`}
100 </span>
101 </span>
102 <span
103 className={`shrink-0 text-xs ${
104 plan.status === "failed"
105 ? "text-danger"
106 : plan.status === "ready"
107 ? "text-accent"
108 : "text-muted"
109 }`}
110 >
111 {STATUS[plan.status]}
112 </span>
113 </Link>
114 </li>
115 ))}
116 </ul>
117 )}
118 </div>
119 </div>
120
121 <aside className="space-y-5 text-sm">
122 <section className="rounded-xl border border-line bg-surface p-5">
123 <h3 className="font-medium">What a plan gives you</h3>
124 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
125 <li>Issues small enough to be merged one at a time, written so that an agent needs nothing else.</li>
126 <li>Acceptance checks on each, taken from how this repository is tested.</li>
127 <li>The files each will touch, and a dependency wherever two would collide.</li>
128 <li>
129 Independent issues are worked on at the same time. One that depends on
130 another starts when that has merged, from its result.
131 </li>
132 </ul>
133 </section>
134 </aside>
135 </div>
136 );
137}