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
84 linesCodeBlameRaw

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.

Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.1import { useEffect, useRef } from "react";
2
3import type { DesktopReport } from "@g1t/contracts";
4
5import { type DesktopRunnerOffer, desktopRunner, reportDue, reportKey, reportOf } from "../lib/desktop-runner";
6
7/**
8 * Inside the desktop app, on every page: the link between the app's runner
9 * and the site (lib/desktop-runner.ts). It tells the app who is signed in
10 * and what they own, makes the registration token the app asks for, and
11 * keeps the server told how the runner is. It draws nothing.
12 */
13export function DesktopRunnerLink({ offer }: { offer: DesktopRunnerOffer }) {
14 const key = JSON.stringify(offer);
15 const last = useRef<{ key: string; at: number } | null>(null);
16
17 // Registering: the token goes straight from the server to the app.
18 useEffect(() => {
19 const runner = desktopRunner();
20 if (!runner) return;
21 let asking: string | null = null;
22 const signedIn = (JSON.parse(key) as DesktopRunnerOffer).signedIn;
23 const register = async (workspace: string) => {
24 // Signed out, the app says so itself, from the offer.
25 if (!signedIn || asking === workspace) return;
26 asking = workspace;
27 try {
28 const response = await fetch("/-/desktop/runner", {
29 method: "POST",
30 headers: { "content-type": "application/json" },
31 body: JSON.stringify({ intent: "token", workspace }),
32 });
33 const answer = (await response.json().catch(() => ({}))) as { token?: string; error?: string; allowed?: boolean };
34 await runner.register(
35 answer.token ? { workspace, token: answer.token } : { workspace, allowed: answer.allowed !== false, error: answer.error ?? `g1t answered ${response.status}.` },
36 );
37 } catch {
38 // Offline, or the page is leaving: the app asks again in a minute.
39 } finally {
40 asking = null;
41 }
42 };
43 const stop = runner.onWanted((workspace) => void register(workspace));
44 void runner
45 .offer(JSON.parse(key) as DesktopRunnerOffer)
46 .then((wanted) => {
47 if (wanted) void register(wanted);
48 })
49 .catch(() => {});
50 return stop;
51 }, [key]);
52
53 // Reporting: what the Runners page shows of this computer, on each change and as a heartbeat.
54 useEffect(() => {
55 const runner = desktopRunner();
56 if (!runner || !offer.signedIn) return;
57 const send = (report: DesktopReport | null) => {
58 if (!report || !offer.workspaces.some((w) => w.slug === report.workspace)) return;
59 const now = Date.now();
60 if (!reportDue(last.current, report, now)) return;
61 last.current = { key: reportKey(report), at: now };
62 void fetch("/-/desktop/runner", {
63 method: "POST",
64 headers: { "content-type": "application/json" },
65 body: JSON.stringify({ intent: "report", ...report }),
66 }).catch(() => {
67 last.current = null;
68 });
69 };
70 const look = () =>
71 void runner
72 .status()
73 .then((view) => send(reportOf(view)))
74 .catch(() => {});
75 look();
76 const stop = runner.onStatus((view) => send(reportOf(view)));
77 const beat = window.setInterval(look, 60_000);
78 return () => {
79 stop();
80 window.clearInterval(beat);
81 };
82 }, [key]); // eslint-disable-line react-hooks/exhaustive-deps
83 return null;
84}

This file's history is long; its oldest lines are credited to the oldest commit read.