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. | 1 | import { useEffect, useRef } from "react"; |
| 2 | ||
| 3 | import type { DesktopReport } from "@g1t/contracts"; | |
| 4 | ||
| 5 | import { 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 | */ | |
| 13 | export 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.