Skip to content

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.sh
66 linesCodeBlameRaw
1/**
2 * Ask g1t (components/ask-composer): `GET -/ask` says what Ask answers with
3 * in the workspace, or why it cannot yet, and whether the viewer is an
4 * owner who can choose; with `?sessions`, whether sessions can start on the
5 * workspace's Anthropic account. The conversation itself is the viewer's
6 * direct message with @g1t in chat (`-/chat/api`, intents `g1t_dm`, `post`,
7 * `g1t_stop`, `g1t_reset`, `g1t_retry`): the chat service has g1t answer
8 * there, with the provider an owner chose, on the workspace's own key.
9 *
10 * `POST -/ask` starts a session g1t proposed in an answer (the proposal
11 * card in components/ask-steps): the person's click is the confirmation.
12 * `handoff` asks for the Claude Code command instead of starting one on
13 * the workspace's Anthropic account.
14 */
15import { data } from "react-router";
16
17import { AGENT_MODES } from "@g1t/contracts";
18
19import type { Route } from "./+types/ask";
20import { agentSessions, integrations } from "../../lib/services.server";
21import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
22
23export async function loader({ params, context, request }: Route.LoaderArgs) {
24 const viewer = getViewer(context);
25 const role = roleIn(viewer, params.owner);
26 if (!role) throw data(null, { status: 404 });
27 const workspace = params.owner.toLowerCase();
28 const headers = { "cache-control": "no-store" };
29 if (new URL(request.url).searchParams.has("sessions")) {
30 const setup = await agentSessions.setup(workspace, viewer).catch(() => null);
31 return Response.json({ available: Boolean(setup?.ok && setup.value.available), model: setup?.ok ? setup.value.model : null }, { headers });
32 }
33 const setup = await integrations.askSettings(workspace, viewer);
34 return Response.json({ ...setup, owner: role === "owner" }, { headers });
35}
36
37export async function action({ request, params, context }: Route.ActionArgs) {
38 assertSameOrigin(request);
39 const user = requireUser(context, request);
40 if (!roleIn(getViewer(context), params.owner)) throw data(null, { status: 404 });
41 const input = (await request.json().catch(() => null)) as Record<string, unknown> | null;
42 const [namespace, name, extra] = String(input?.repo ?? "").split("/");
43 // A proposal is for a repository in this workspace only.
44 if (!namespace || !name || extra !== undefined || namespace.toLowerCase() !== params.owner.toLowerCase()) {
45 return Response.json({ error: "That repository isn't in this workspace." }, { status: 400 });
46 }
47 const text = (key: string) => (typeof input?.[key] === "string" && (input[key] as string).trim()) || undefined;
48 const issue = Number.isInteger(input?.issue) && (input?.issue as number) > 0 ? (input?.issue as number) : undefined;
49 const title = issue ? undefined : text("title");
50 if (!issue && !title) return Response.json({ error: "The proposal names no issue." }, { status: 400 });
51 const mode = AGENT_MODES.find((one) => one.mode === input?.mode)?.mode ?? "implement";
52 const result = await agentSessions
53 .start(user, { namespace, name }, { issue, title, body: issue ? undefined : text("body"), instruction: text("instruction"), mode, handoff: input?.handoff === true })
54 .catch(() => null);
55 if (!result) return Response.json({ error: "The session could not be started. Try again in a moment." }, { status: 502 });
56 if (!result.ok) return Response.json({ error: result.error.message }, { status: 400 });
57 const started = result.value;
58 return Response.json({
59 issue: started.issue,
60 session_url: started.sessionUrl ?? (started.session ? `/${namespace}/${name}/work/sessions/${started.session.id}` : null),
61 provider_session_url: started.providerSessionUrl,
62 handoff: started.handoff,
63 command: started.command,
64 prompt: started.prompt,
65 });
66}