Skip to content

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

151 lines6,579 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";
Merge branch 'worktree-agent-a985c01f43538af01'3import { Form, Link, redirect } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
5import type { Plan } from "@g1t/contracts";
6
7import type { Route } from "./+types/plans";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look8import { refusal, requireRepo } from "../../lib/access.server";
9import { whyNot } from "../../lib/access";
Social cards for every page: og.g1t.sh10import { page } from "../../lib/meta";
Merge branch 'worktree-agent-a985c01f43538af01'11import { ComputeNote, EmptyState, ErrorText, SubmitButton, Textarea, TimeAgo } from "../../components/ui";
Merge branch 'worktree-agent-ae1299e92e4462012'12import { Hint } from "../../components/ui/hint";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look13import { computeNoteFor } from "../../lib/compute.server";
Agents as a team: lifecycle, merge queue, billing and a new shell14import { work } from "../../lib/services.server";
15import {
16 assertSameOrigin,
17 getViewer,
18 requireUser,
19 unwrap,
20} from "../../lib/session.server";
21
Social cards for every page: og.g1t.sh22export function meta({ params, ...args }: Route.MetaArgs) {
23 return page(args, { title: `Plan · ${params.owner}/${params.repo} · g1t` });
Agents as a team: lifecycle, merge queue, billing and a new shell24}
25
26export async function loader({ params, context }: Route.LoaderArgs) {
27 const viewer = getViewer(context);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28 // Anyone who can read the repository: a public one's to anyone, a private one's to people with a role.
29 const { access } = await requireRepo(context, params, "read");
Agents as a team: lifecycle, merge queue, billing and a new shell30 const path = { namespace: params.owner, name: params.repo };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31 const [plans, computeNote] = await Promise.all([
32 work.listPlans(path, viewer),
33 // Planning is an agent run: said before it is refused for the plan.
34 computeNoteFor(params.owner, "agent"),
35 ]);
36 return { plans: unwrap(plans), computeNote, can: access.can };
Agents as a team: lifecycle, merge queue, billing and a new shell37}
38
39export async function action({ request, params, context }: Route.ActionArgs) {
40 assertSameOrigin(request);
41 const user = requireUser(context, request);
42 const form = await request.formData();
43 const path = { namespace: params.owner, name: params.repo };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 // Planning is an agent run: Write and up.
45 const refused = await refusal(context, params, "run");
46 if (refused) return { error: refused };
Agents as a team: lifecycle, merge queue, billing and a new shell47 const started = await env.RUNNER.plan(user, path, String(form.get("brief") ?? ""));
48 if (!started.ok) return { error: started.error.message };
49 throw redirect(`/${params.owner}/${params.repo}/plans/${started.value.planId}`);
50}
51
52const STATUS: Record<Plan["status"], string> = {
53 planning: "Being written",
54 ready: "Ready to read",
55 failed: "Could not be written",
56 applied: "Applied",
57};
58
59export default function Plans({ loaderData, actionData, params }: Route.ComponentProps) {
60 const { plans } = loaderData;
61 const base = `/${params.owner}/${params.repo}`;
Merge branch 'worktree-agent-a985c01f43538af01'62
Agents as a team: lifecycle, merge queue, billing and a new shell63 return (
64 <div className="grid gap-10 lg:grid-cols-[1fr_20rem]">
65 <div className="min-w-0">
66 <h2 className="text-xl font-semibold tracking-tight">Plan work</h2>
67 <p className="mt-1 max-w-2xl text-sm text-muted">
68 Say what you want to be true when the work is done. An agent reads this
69 repository and proposes the issues that would get there, with what each
70 must pass and which have to land before which. You read it, change what
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent71 you like, and put g1t on it with one click.
Agents as a team: lifecycle, merge queue, billing and a new shell72 </p>
73 <Form method="post" className="mt-5 space-y-3">
74 <Textarea
75 name="brief"
76 rows={7}
77 required
78 placeholder={
79 "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."
80 }
81 />
82 <div className="flex flex-wrap items-center gap-3">
Merge branch 'worktree-agent-ae1299e92e4462012'83 <Hint label={whyNot(loaderData.can, "run")} disabled={!loaderData.can.run}>
84 <SubmitButton variant="accent" pending="Starting the planner…" disabled={!loaderData.can.run}>
85 <Sparkles size={15} />
86 Plan it
87 </SubmitButton>
88 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell89 <span className="text-xs text-muted">
90 Nothing is opened until you have read the plan.
91 </span>
92 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 {actionData?.error ? <ErrorText>{actionData.error}</ErrorText> : <ComputeNote note={loaderData.computeNote} />}
Agents as a team: lifecycle, merge queue, billing and a new shell94 </Form>
95
96 <h3 className="mt-10 text-sm font-medium text-muted">Plans</h3>
97 <div className="mt-3">
98 {plans.length === 0 ? (
99 <EmptyState title="No plans yet" />
100 ) : (
101 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line">
102 {plans.map((plan) => (
103 <li key={plan.id}>
104 <Link
105 to={`${base}/plans/${plan.id}`}
106 className="flex items-center gap-4 px-4 py-3 transition-colors hover:bg-surface"
107 >
108 <span className="min-w-0 grow">
109 <span className="block truncate font-medium">{plan.brief}</span>
110 <span className="mt-0.5 block text-xs text-faint">
111 {plan.author.username} · <TimeAgo at={plan.createdAt} />
112 {plan.issues.length > 0 &&
113 ` · ${plan.issues.length} ${plan.issues.length === 1 ? "issue" : "issues"}`}
114 </span>
115 </span>
116 <span
117 className={`shrink-0 text-xs ${
118 plan.status === "failed"
119 ? "text-danger"
120 : plan.status === "ready"
Merge branch 'worktree-agent-ad7c6d88d93adc817'121 ? "text-success"
Agents as a team: lifecycle, merge queue, billing and a new shell122 : "text-muted"
123 }`}
124 >
125 {STATUS[plan.status]}
126 </span>
127 </Link>
128 </li>
129 ))}
130 </ul>
131 )}
132 </div>
133 </div>
134
135 <aside className="space-y-5 text-sm">
136 <section className="rounded-xl border border-line bg-surface p-5">
137 <h3 className="font-medium">What a plan gives you</h3>
138 <ul className="mt-2 list-disc space-y-1.5 pl-4 text-muted">
139 <li>Issues small enough to be merged one at a time, written so that an agent needs nothing else.</li>
Fast pages, required checks on the branch, self-hosted runners, honest incidents140 <li>What done looks like for each, in plain words, added to its description.</li>
Agents as a team: lifecycle, merge queue, billing and a new shell141 <li>The files each will touch, and a dependency wherever two would collide.</li>
142 <li>
143 Independent issues are worked on at the same time. One that depends on
144 another starts when that has merged, from its result.
145 </li>
146 </ul>
147 </section>
148 </aside>
149 </div>
150 );
151}

This file's history is long; its oldest lines are credited to the oldest commit read.