| 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 | */ |
| 6 | import type { AgentStart, DelegateInput, RepoPath } from "@g1t/contracts"; |
| 7 | |
| 8 | /** What a form asks for, read the way both forms name their fields. */ |
| 9 | export 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`. */ |
| 23 | export 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. */ |
| 30 | export 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. */ |
| 35 | export 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 | |
| 43 | const 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 | */ |
| 55 | export 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 | } |