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

64 lines2,707 bytesCodeBlame
1/**
2 * Putting an agent on something in one step, from a form: the issue new
3 * page with "Assign g1t 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 };
16}
17
18/** The project a composer chose, written `owner/name`, if it is one of `repos`. */
19export function chosenRepo(value: FormDataEntryValue | null, repos: RepoPath[]): RepoPath | null {
20 const [namespace, name, extra] = String(value ?? "").split("/");
21 if (!namespace || !name || extra != null) return null;
22 return repos.find((repo) => repo.namespace.toLowerCase() === namespace.toLowerCase() && repo.name.toLowerCase() === name.toLowerCase()) ?? null;
23}
24
25/** Where to land once the issue is open: on it, while the agent works or waits. */
26export function issuePath(repo: RepoPath, number: number): string {
27 return `/${repo.namespace}/${repo.name}/issues/${number}`;
28}
29
30/** What a form says when the issue was opened but the agent did not start. */
31export type NotStarted = {
32 /** The issue that was opened. */
33 to: string;
34 number: number;
35 message: string;
36 fix: { label: string; to: string } | null;
37};
38
39const FIX_LABEL: Record<string, string> = {
40 not_paid: "Start the plan or the trial",
41 trial_used: "Start the plan",
42 limit: "Raise the limit",
43 issue_cap: "Raise the cap per issue",
44 no_model: "Connect a model",
45};
46
47/**
48 * The agent's start, for a form: null when it started or is queued, so the
49 * form goes on to the issue; otherwise what to say and where to fix it.
50 */
51export function notStarted(agent: AgentStart, repo: RepoPath, number: number): NotStarted | null {
52 if (agent.status !== "not_started") return null;
53 // The fix is on g1t.sh itself: followed within the site.
54 const fixTo = agent.fixUrl?.replace(/^https:\/\/g1t\.sh(?=\/)/, "") ?? null;
55 return {
56 to: issuePath(repo, number),
57 number,
58 // The fix is offered as a link of its own, so the address the message ends with is left off.
59 message: fixTo
60 ? (agent.message ?? "g1t did not start.").replace(/:\s*(?:https:\/\/g1t\.sh)?\/[\w./#-]+\s*$/, ".")
61 : (agent.message ?? "g1t did not start."),
62 fix: fixTo ? { label: FIX_LABEL[agent.code ?? ""] ?? "Fix it", to: fixTo } : null,
63 };
64}