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/settings-agents.tsx

120 lines5,398 bytesCodeBlame
1import { env } from "cloudflare:workers";
2import { AtSign, Tag } from "lucide-react";
3import { Form, data, useNavigation } from "react-router";
4
5import { AGENT_HANDLE, mentionsClient } from "@g1t/contracts";
6
7import type { Route } from "./+types/settings-agents";
8import { page } from "../../lib/meta";
9import { RepoSettingsTabs } from "../../components/repo-settings-tabs";
10import { Button, ErrorText, Field, Input, TimeAgo } from "../../components/ui";
11import { work } from "../../lib/services.server";
12import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
13
14const mentions = mentionsClient(env.WORK);
15
16export function meta({ params, ...args }: Route.MetaArgs) {
17 return page(args, { title: `Agent settings · ${params.owner}/${params.repo} · g1t` });
18}
19
20export async function loader({ params, context }: Route.LoaderArgs) {
21 const viewer = getViewer(context);
22 // Members only; to anyone else the page does not exist.
23 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
24 const path = { namespace: params.owner, name: params.repo };
25 const [rules, labels] = await Promise.all([mentions.getAgentRules(path, viewer), work.listLabels(path, viewer)]);
26 return { rules: unwrap(rules), labels: labels.ok ? labels.value : [] };
27}
28
29export async function action({ request, params, context }: Route.ActionArgs) {
30 assertSameOrigin(request);
31 const user = requireUser(context, request);
32 const form = await request.formData();
33 const path = { namespace: params.owner, name: params.repo };
34 const label = form.get("intent") === "off" ? null : String(form.get("label") ?? "").trim() || null;
35 const saved = await mentions.setAgentRules(user, path, { label });
36 if (!saved.ok) return { error: saved.error.message, notice: null };
37 return {
38 error: null,
39 notice: saved.value.label
40 ? `Issues labelled ${saved.value.label} now go to g1t-agent.`
41 : "Labels no longer put g1t-agent to work.",
42 };
43}
44
45export default function AgentSettings({ loaderData, actionData, params }: Route.ComponentProps) {
46 const { rules, labels } = loaderData;
47 const busy = useNavigation().state === "submitting";
48 const base = `/${params.owner}/${params.repo}`;
49 return (
50 <div className="max-w-4xl">
51 <RepoSettingsTabs base={base} />
52 <div className="min-h-6">
53 {actionData?.notice && <p className="text-sm text-accent">{actionData.notice}</p>}
54 <ErrorText>{actionData?.error ?? null}</ErrorText>
55 </div>
56
57 <section className="grid gap-x-10 gap-y-4 lg:grid-cols-[16rem_1fr]">
58 <div>
59 <h2 className="flex items-center gap-2 font-medium">
60 <Tag size={15} className="text-muted" />
61 Label rule
62 </h2>
63 <p className="mt-1 text-sm text-muted">
64 When a member gives an issue this label, g1t-agent takes it, as if they had assigned it. It starts as soon as
65 the project has room for another agent and nothing the issue depends on is still open.
66 </p>
67 </div>
68 <Form method="post" className="space-y-3 rounded-xl border border-line bg-surface p-4">
69 <Field label="Label" hint="An issue that already has it is not affected; adding it again is.">
70 <Input name="label" list="known-labels" defaultValue={rules.label ?? ""} placeholder="agent" maxLength={40} />
71 </Field>
72 <datalist id="known-labels">
73 {labels.map((label) => (
74 <option key={label} value={label} />
75 ))}
76 </datalist>
77 <div className="flex flex-wrap items-center gap-3">
78 <Button type="submit" name="intent" value="save" disabled={busy}>
79 Save
80 </Button>
81 {rules.label && (
82 <Button type="submit" variant="quiet" name="intent" value="off" disabled={busy}>
83 Turn off
84 </Button>
85 )}
86 {rules.updatedBy && rules.updatedAt && (
87 <span className="text-xs text-faint">
88 Changed by {rules.updatedBy} <TimeAgo at={rules.updatedAt} />
89 </span>
90 )}
91 </div>
92 </Form>
93 </section>
94
95 <section className="mt-8 grid gap-x-10 gap-y-4 border-t border-line pt-8 lg:grid-cols-[16rem_1fr]">
96 <div>
97 <h2 className="flex items-center gap-2 font-medium">
98 <AtSign size={15} className="text-muted" />
99 Mentions
100 </h2>
101 <p className="mt-1 text-sm text-muted">Always on, for members of the workspace.</p>
102 </div>
103 <ul className="space-y-2 text-sm text-muted">
104 <li>
105 <span className="font-mono text-fg">{AGENT_HANDLE} take this</span> on an issue assigns it to g1t-agent. A
106 question gets an answer in the thread instead.
107 </li>
108 <li>
109 On a pull request g1t-agent made, a request sends it back to make the change. On any pull request,{" "}
110 <span className="font-mono text-fg">{AGENT_HANDLE} review</span> starts a review and a question is answered.
111 </li>
112 <li>
113 Mentions in code, in quotes or from people outside the workspace start nothing. Each comment starts one run at
114 most, and g1t-agent replies saying what it did, or why it could not.
115 </li>
116 </ul>
117 </section>
118 </div>
119 );
120}