g1t/apps/web/app/lib/delegate.ts

68 lines2,831 bytesCodeBlame
1/**
2 * Putting an agent on something in one step, from a form: the issue new
3 * page with "Assign g1t-agent now", and Mission control's composer. Pure,
4 * so it is tested on its own; the routes call `env.RUNNER.delegate`.
5 */
6import type { AgentStart, DelegateInput, RepoPath } from "@g1t/contracts";
7
8/** What a form asks for, read the way both forms name their fields. */
9export function delegateForm(form: FormData): DelegateInput {
10 const text = (name: string) => String(form.get(name) ?? "");
11 return {
12 title: text("title").trim(),
13 body: text("body").trim(),
14 labels: [...form.getAll("label").map(String), ...text("labels").split(",")].map((label) => label.trim()).filter(Boolean),
15 checks: text("checks")
16 .split("\n")
17 .map((check) => check.trim())
18 .filter(Boolean),
19 };
20}
21
22/** The project a composer chose, written `owner/name`, if it is one of `repos`. */
23export function chosenRepo(value: FormDataEntryValue | null, repos: RepoPath[]): RepoPath | null {
24 const [namespace, name, extra] = String(value ?? "").split("/");
25 if (!namespace || !name || extra != null) return null;
26 return repos.find((repo) => repo.namespace.toLowerCase() === namespace.toLowerCase() && repo.name.toLowerCase() === name.toLowerCase()) ?? null;
27}
28
29/** Where to land once the issue is open: on it, while the agent works or waits. */
30export function issuePath(repo: RepoPath, number: number): string {
31 return `/${repo.namespace}/${repo.name}/issues/${number}`;
32}
33
34/** What a form says when the issue was opened but the agent did not start. */
35export type NotStarted = {
36 /** The issue that was opened. */
37 to: string;
38 number: number;
39 message: string;
40 fix: { label: string; to: string } | null;
41};
42
43const FIX_LABEL: Record<string, string> = {
44 not_paid: "Start the plan or the trial",
45 trial_used: "Start the plan",
46 limit: "Raise the limit",
47 issue_cap: "Raise the cap per issue",
48 no_model: "Connect a model",
49};
50
51/**
52 * The agent's start, for a form: null when it started or is queued, so the
53 * form goes on to the issue; otherwise what to say and where to fix it.
54 */
55export function notStarted(agent: AgentStart, repo: RepoPath, number: number): NotStarted | null {
56 if (agent.status !== "not_started") return null;
57 // The fix is on g1t.sh itself: followed within the site.
58 const fixTo = agent.fixUrl?.replace(/^https:\/\/g1t\.sh(?=\/)/, "") ?? null;
59 return {
60 to: issuePath(repo, number),
61 number,
62 // The fix is offered as a link of its own, so the address the message ends with is left off.
63 message: fixTo
64 ? (agent.message ?? "g1t-agent did not start.").replace(/:\s*(?:https:\/\/g1t\.sh)?\/[\w./#-]+\s*$/, ".")
65 : (agent.message ?? "g1t-agent did not start."),
66 fix: fixTo ? { label: FIX_LABEL[agent.code ?? ""] ?? "Fix it", to: fixTo } : null,
67 };
68}