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

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