Skip to content
218 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.1/**
2 * The runner the desktop app keeps on this computer, from the site's side
3 * (apps/desktop/src-tauri/src/runner). The app finds out what to run for,
4 * and gets its registration token, through the page:
5 *
6 * 1. On every page, the page tells the app who is signed in, the
7 * workspaces they own (only owners add runners) and the one they are
8 * in (`offer`). The app answers with the workspace it wants a
9 * registration token for, if any, and asks again later (`onWanted`).
10 * 2. For that workspace, the page asks the server for a token
11 * (`POST /-/desktop/runner`, owners only, same origin) and hands it to
12 * the app (`register`), or says why there is none. It is never shown
13 * or kept here.
14 * 3. Whenever the runner's state changes, and every few minutes while g1t
15 * is open, the page sends the app's diagnosis to the server
16 * (`report`), so the workspace's Runners page lists every person's
17 * desktop with its state, why and the fix, in the app's own words.
18 */
19import { useEffect, useState } from "react";
20
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.21import type { DesktopBackend, DesktopCapability, DesktopCapabilityState, DesktopDiagnosis, DesktopJob, DesktopReport, Runner } from "@g1t/contracts";
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.22
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.23import { agentRunHref, workflowRunHref } from "./runners.ts";
24
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.25/** A fix, as a button: its id is the app's (`runner_control`). */
26export type DesktopRunnerAction = { id: string; label: string };
27
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.28/**
29 * A kind of job the sandbox runs, with its fixes' buttons
30 * (`diagnosis::CapabilityLine` in the app), and what only this computer
31 * knows: whether it can start and stop on its own, and how far its
32 * image has downloaded.
33 */
34export type DesktopRunnerCapability = DesktopCapability & {
35 actions: DesktopRunnerAction[];
36 available?: boolean;
37 can_toggle?: boolean;
38 progress?: number | null;
39 job?: (DesktopJob & { link?: string | null }) | null;
40};
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.41
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.42/** What the app says of its runner (`runner::View` in the app). */
43export type DesktopRunnerView = {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.44 diagnosis: Omit<DesktopDiagnosis, "capabilities"> & { actions: DesktopRunnerAction[]; capabilities?: DesktopRunnerCapability[] };
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.45 closed: DesktopDiagnosis & { actions: DesktopRunnerAction[] };
46 running: boolean;
47 version: string | null;
48 backend: DesktopBackend;
49 workspace: string | null;
50 workspaces: { slug: string; name: string | null }[];
51 enabled: boolean;
52 paused: boolean;
53 link: string | null;
54 machine: string;
55 platform: string;
56 log: string[];
57};
58
59export type DesktopRunnerOffer = { signedIn: boolean; workspaces: { slug: string; name: string | null }[]; current: string | null };
60
61/** `window.g1tDesktop.runner`, from the app's bridge.js. */
62export type DesktopRunnerBridge = {
63 status(): Promise<DesktopRunnerView>;
64 offer(offer: DesktopRunnerOffer): Promise<string | null>;
65 register(answer: { workspace: string; token?: string | null; allowed?: boolean; error?: string | null }): Promise<void>;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.66 /** A fix, or Start and Stop: of one kind of job alone with its id as `capability`. */
67 act(action: string, capability?: string): Promise<void>;
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.68 onWanted(listener: (workspace: string) => void): () => void;
69 onStatus(listener: (view: DesktopRunnerView) => void): () => void;
70};
71
72/** The bridge's runner, inside the desktop app (lib/notify-client.ts describes the rest of `window.g1tDesktop`). */
73export function desktopRunner(): DesktopRunnerBridge | null {
74 if (typeof window === "undefined") return null;
75 const runner = window.g1tDesktop?.runner;
76 return runner && typeof runner.status === "function" ? runner : null;
77}
78
79/** This computer's runner, live, inside the desktop app; null elsewhere. */
80export function useDesktopRunner(): DesktopRunnerView | null {
81 const [view, setView] = useState<DesktopRunnerView | null>(null);
82 useEffect(() => {
83 const runner = desktopRunner();
84 if (!runner) return;
85 let live = true;
86 runner
87 .status()
88 .then((first) => {
89 if (live) setView(first);
90 })
91 .catch(() => {});
92 const stop = runner.onStatus((next) => setView(next));
93 return () => {
94 live = false;
95 stop();
96 };
97 }, []);
98 return view;
99}
100
101/** How often a desktop says it is still open, with nothing new: within the server's ten minutes. */
102export const HEARTBEAT_MS = 4 * 60 * 1000;
103
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.104/** A kind of job as the server keeps it: what it says, without what only works or changes on that computer. */
105function plainCapability(capability: DesktopCapability): DesktopCapability {
106 const { id, label, tone, title, cause, fix, state, headline, about, enabled, job, runner, isolation, version } = capability;
107 const said: DesktopCapability = { id, label, tone, title, cause: cause ?? null, fix: fix ?? null };
108 if (state) said.state = state;
109 if (headline) said.headline = headline;
110 if (about) said.about = about;
111 if (typeof enabled === "boolean") said.enabled = enabled;
112 if (job) said.job = { kind: job.kind, name: job.name, repo: job.repo };
113 if (runner) said.runner = runner;
114 if (isolation) said.isolation = isolation;
115 if (version) said.version = version;
116 return said;
117}
118
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.119/** What goes to the server, without the buttons, which only work on that computer. */
120export function reportOf(view: DesktopRunnerView): DesktopReport | null {
121 if (!view.workspace) return null;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.122 const plain = ({ code, tone, title, cause, fix, capabilities, in_capabilities }: DesktopDiagnosis): DesktopDiagnosis => {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.123 const said: DesktopDiagnosis = { code, tone, title, cause, fix };
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.124 if (capabilities?.length) {
125 said.capabilities = capabilities.map(plainCapability);
126 if (in_capabilities) said.in_capabilities = true;
127 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.128 return said;
129 };
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.130 return { workspace: view.workspace, machine: view.machine, backend: view.backend, version: view.version, diagnosis: plain(view.diagnosis), closed: plain(view.closed) };
131}
132
133/** Whether a report is due: something the Runners page shows changed, or the heartbeat. */
134export function reportDue(last: { key: string; at: number } | null, report: DesktopReport, now: number): boolean {
135 if (!last) return true;
136 return last.key !== reportKey(report) || now - last.at >= HEARTBEAT_MS;
137}
138
139export function reportKey(report: DesktopReport): string {
140 const { diagnosis } = report;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.141 const capabilities = (diagnosis.capabilities ?? []).map((c) => [c.id, c.tone, c.title, c.cause, c.fix, c.state, c.enabled, c.headline, c.job?.name, c.job?.repo, c.version].join("\u0001"));
142 return [report.workspace, report.machine, report.backend, report.version, diagnosis.code, diagnosis.title, diagnosis.cause, diagnosis.fix, diagnosis.in_capabilities, ...capabilities].join("\u0000");
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.143}
144
145/** A string with something in it, or null. */
146export const text = (value: unknown): string | null => (typeof value === "string" && value.length > 0 ? value : null);
147
148const BACKENDS: DesktopBackend[] = ["sandboxed", "docker", "host"];
149const TONES: DesktopDiagnosis["tone"][] = ["ok", "working", "off", "warning", "problem"];
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.150const STATES: DesktopCapabilityState[] = ["busy", "idle", "starting", "downloading", "updating", "waiting", "stopped", "unavailable", "problem"];
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.151
152export function backendOf(value: unknown): DesktopBackend | null {
153 return BACKENDS.find((backend) => backend === value) ?? null;
154}
155
156/** A diagnosis as the app sent it, or null when it is not one. */
157export function diagnosisOf(value: unknown): DesktopDiagnosis | null {
158 if (!value || typeof value !== "object") return null;
159 const given = value as Record<string, unknown>;
160 const code = text(given.code);
161 const title = text(given.title);
162 const tone = TONES.find((t) => t === given.tone);
163 if (!code || !title || !tone) return null;
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.164 const said: DesktopDiagnosis = { code, tone, title, cause: text(given.cause), fix: text(given.fix) };
165 const capabilities = Array.isArray(given.capabilities)
166 ? given.capabilities.map(capabilityOf).filter((c): c is DesktopCapability => c !== null).slice(0, MAX_CAPABILITIES)
167 : [];
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.168 if (capabilities.length > 0) {
169 said.capabilities = capabilities;
170 if (given.in_capabilities === true) said.in_capabilities = true;
171 }
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.172 return said;
173}
174
175/** The most kinds of job a desktop reports (the server keeps as many). */
176const MAX_CAPABILITIES = 4;
177
178/** A kind of job as the app sent it, or null when it is not one. */
179function capabilityOf(value: unknown): DesktopCapability | null {
180 if (!value || typeof value !== "object") return null;
181 const given = value as Record<string, unknown>;
182 const id = text(given.id);
183 const label = text(given.label);
184 const title = text(given.title);
185 const tone = TONES.find((t) => t === given.tone);
186 if (!id || !label || !title || !tone) return null;
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.187 const said: DesktopCapability = { id, label, tone, title, cause: text(given.cause), fix: text(given.fix) };
188 const state = STATES.find((s) => s === given.state);
189 if (state) said.state = state;
190 for (const key of ["headline", "about", "runner", "isolation", "version"] as const) {
191 const value = text(given[key]);
192 if (value) said[key] = value;
193 }
194 if (typeof given.enabled === "boolean") said.enabled = given.enabled;
195 const job = given.job && typeof given.job === "object" ? (given.job as Record<string, unknown>) : null;
196 const [kind, name, repo] = [text(job?.kind), text(job?.name), text(job?.repo)];
197 if (kind && name && repo) said.job = { kind, name, repo };
198 return said;
199}
200
201/**
202 * Where a kind of job's current work is on g1t: its run, from the runner
203 * it is (by its name, `build-box-linux`), else its repository's runs.
204 */
205export function jobHref(capability: Pick<DesktopCapability, "job" | "runner">, runners: Pick<Runner, "name" | "work">[]): string | null {
206 const job = capability.job;
207 if (!job) return null;
208 const work = capability.runner ? runners.find((runner) => runner.name === capability.runner)?.work : null;
209 if (work?.runId && work.repo) return work.kind === "workflow" ? workflowRunHref(work.repo, work.runId) : agentRunHref(work.repo, work.runId);
210 return job.kind === "agent" ? `/${job.repo}/agents` : `/${job.repo}/actions`;
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.211}
212
213/** The words for how a desktop runs jobs, as its settings say them. */
214export const BACKEND_LABEL: Record<DesktopBackend, string> = {
215 sandboxed: "Sandboxed",
216 docker: "In Docker (not a sandbox)",
217 host: "Directly on the computer (not a sandbox)",
218};

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