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.
| Initial g1t: services, event bus, intents and attempts | 1 | import { env } from "cloudflare:workers"; |
| 2 | import { Form, Link, redirect } from "react-router"; | |
| 3 | ||
| 4 | import type { Route } from "./+types/intent"; | |
| 5 | import { Button, ErrorText, Input, Status, TimeAgo } from "../../components/ui"; | |
| 6 | import { | |
| 7 | assertSameOrigin, | |
| 8 | getViewer, | |
| 9 | requireUser, | |
| 10 | unwrap, | |
| 11 | } from "../../lib/session.server"; | |
| 12 | ||
| 13 | export function meta({ loaderData: data, params }: Route.MetaArgs) { | |
| 14 | const title = data ? `${data.intent.title} · ` : ""; | |
| 15 | return [{ title: `${title}${params.owner}/${params.repo} · g1t` }]; | |
| 16 | } | |
| 17 | ||
| 18 | export async function loader({ params, context }: Route.LoaderArgs) { | |
| 19 | const viewer = getViewer(context); | |
| 20 | const detail = unwrap( | |
| 21 | await env.WORK.getIntent( | |
| 22 | { namespace: params.owner, name: params.repo }, | |
| 23 | Number(params.number), | |
| 24 | viewer, | |
| 25 | ), | |
| 26 | ); | |
| 27 | return { ...detail, viewer }; | |
| 28 | } | |
| 29 | ||
| 30 | export 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 intentId = String(form.get("intentId") ?? ""); | |
| 35 | const base = `/${params.owner}/${params.repo}`; | |
| 36 | ||
| 37 | if (form.get("action") === "withdraw") { | |
| 38 | const result = await env.WORK.withdrawIntent(user, intentId); | |
| 39 | return result.ok ? null : { error: result.error.message }; | |
| 40 | } | |
| 41 | const result = await env.WORK.startAttempt(user, intentId, { | |
| 42 | agent: String(form.get("agent") ?? ""), | |
| 43 | runtime: "external", | |
| 44 | }); | |
| 45 | if (!result.ok) return { error: result.error.message }; | |
| 46 | throw redirect(`${base}/attempts/${result.value.id}`); | |
| 47 | } | |
| 48 | ||
| 49 | export default function IntentPage({ | |
| 50 | loaderData, | |
| 51 | actionData, | |
| 52 | params, | |
| 53 | }: Route.ComponentProps) { | |
| 54 | const { intent, attempts, viewer } = loaderData; | |
| 55 | const base = `/${params.owner}/${params.repo}`; | |
| 56 | const open = intent.status === "open"; | |
| 57 | return ( | |
| 58 | <div className="mt-6 grid gap-8 lg:grid-cols-[1fr_20rem]"> | |
| 59 | <div className="min-w-0"> | |
| 60 | <div className="flex items-start gap-3"> | |
| 61 | <h2 className="grow text-2xl font-semibold tracking-tight"> | |
| 62 | {intent.title}{" "} | |
| 63 | <span className="font-mono font-normal text-muted"> | |
| 64 | #{intent.number} | |
| 65 | </span> | |
| 66 | </h2> | |
| 67 | <Status value={intent.status} /> | |
| 68 | </div> | |
| 69 | <p className="mt-1 text-sm text-muted"> | |
| 70 | Opened by {intent.author.username} <TimeAgo at={intent.createdAt} /> | |
| 71 | </p> | |
| 72 | ||
| 73 | {intent.brief && ( | |
| 74 | <p className="mt-6 whitespace-pre-wrap leading-relaxed"> | |
| 75 | {intent.brief} | |
| 76 | </p> | |
| 77 | )} | |
| 78 | ||
| 79 | <h3 className="mt-10 text-sm font-medium text-muted"> | |
| 80 | Attempts ({attempts.length}) | |
| 81 | </h3> | |
| 82 | {attempts.length === 0 ? ( | |
| 83 | <p className="mt-3 rounded-md border border-dashed border-line p-6 text-center text-sm text-muted"> | |
| 84 | No agent has attempted this yet. | |
| 85 | </p> | |
| 86 | ) : ( | |
| 87 | <ul className="mt-3 space-y-2"> | |
| 88 | {attempts.map((attempt) => ( | |
| 89 | <li key={attempt.id}> | |
| 90 | <Link | |
| 91 | to={`${base}/attempts/${attempt.id}`} | |
| 92 | className="block rounded-md border border-line px-4 py-3 hover:border-muted" | |
| 93 | > | |
| 94 | <div className="flex items-center gap-3"> | |
| 95 | <span className="font-mono text-sm text-muted"> | |
| 96 | {attempt.number} | |
| 97 | </span> | |
| 98 | <span className="font-mono text-sm">{attempt.agent}</span> | |
| 99 | <Status value={attempt.status} /> | |
| 100 | <span className="ml-auto font-mono text-xs text-muted"> | |
| 101 | {attempt.headCommit?.slice(0, 7) ?? "no commits"} ·{" "} | |
| 102 | <TimeAgo at={attempt.updatedAt} /> | |
| 103 | </span> | |
| 104 | </div> | |
| 105 | {attempt.summary && ( | |
| 106 | <p className="mt-2 line-clamp-2 text-sm text-muted"> | |
| 107 | {attempt.summary} | |
| 108 | </p> | |
| 109 | )} | |
| 110 | </Link> | |
| 111 | </li> | |
| 112 | ))} | |
| 113 | </ul> | |
| 114 | )} | |
| 115 | </div> | |
| 116 | ||
| 117 | <aside className="space-y-6"> | |
| 118 | {intent.checks.length > 0 && ( | |
| 119 | <section> | |
| 120 | <h3 className="text-sm font-medium text-muted">Acceptance checks</h3> | |
| 121 | <ul className="mt-2 space-y-1 font-mono text-sm"> | |
| 122 | {intent.checks.map((check) => ( | |
| 123 | <li key={check} className="rounded bg-surface px-2 py-1"> | |
| 124 | {check} | |
| 125 | </li> | |
| 126 | ))} | |
| 127 | </ul> | |
| 128 | </section> | |
| 129 | )} | |
| 130 | {open && viewer && ( | |
| 131 | <section> | |
| 132 | <h3 className="text-sm font-medium text-muted">Start an attempt</h3> | |
| 133 | <Form method="post" className="mt-2 space-y-2"> | |
| 134 | <input type="hidden" name="intentId" value={intent.id} /> | |
| 135 | <Input name="agent" placeholder="claude-code" maxLength={60} /> | |
| 136 | <Button type="submit">Start attempt</Button> | |
| 137 | </Form> | |
| 138 | <p className="mt-2 text-xs text-muted"> | |
| 139 | Creates a fork for the agent to push to. | |
| 140 | </p> | |
| 141 | </section> | |
| 142 | )} | |
| 143 | {open && !viewer && ( | |
| 144 | <p className="text-sm text-muted"> | |
| 145 | <Link to="/login" className="text-fg underline"> | |
| 146 | Sign in | |
| 147 | </Link>{" "} | |
| 148 | to start an attempt. | |
| 149 | </p> | |
| 150 | )} | |
| 151 | {open && viewer?.id === intent.author.id && ( | |
| 152 | <Form method="post"> | |
| 153 | <input type="hidden" name="intentId" value={intent.id} /> | |
| 154 | <input type="hidden" name="action" value="withdraw" /> | |
| 155 | <Button variant="quiet" type="submit"> | |
| 156 | Withdraw intent | |
| 157 | </Button> | |
| 158 | </Form> | |
| 159 | )} | |
| 160 | <ErrorText>{actionData?.error}</ErrorText> | |
| 161 | </aside> | |
| 162 | </div> | |
| 163 | ); | |
| 164 | } |