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
441 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 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.1import { AppWindow, Laptop, SquareTerminal } from "lucide-react";
2import { type ReactNode, useState } from "react";
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.3import { Link } from "react-router";
4
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.5import type { DesktopBackend, DesktopDiagnosis, DesktopRunner, DesktopUpdate, 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.6
Merge branch 'main' into rm-agents7import { useBusy } from "../lib/busy";
8import { BACKEND_LABEL, type DesktopRunnerAction, type DesktopRunnerCapability, type DesktopRunnerView, desktopRunner, jobHref, kindStatusKey, runnerStatusKey, useDesktopRunner } from "../lib/desktop-runner";
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.9import { EmptyState, TimeAgo } from "./ui";
Merge branch 'main' into rm-agents10import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "./ui/alert-dialog";
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.11import { Badge, type BadgeTone } from "./ui/badge";
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.12import { Button } from "./ui/button";
13import { Card } from "./ui/card";
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.14import { Progress } from "./ui/progress";
15import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
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.16
17/** A diagnosis's tone, as a dot. */
Merge branch 'main' into rm-agents18export const DOT: Record<DesktopDiagnosis["tone"], string> = {
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.19 ok: "bg-success",
20 working: "bg-info",
21 off: "bg-faint",
22 warning: "bg-warn",
23 problem: "bg-danger",
24};
25
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.26/** A kind of job's tone, as its label's. */
Merge branch 'main' into rm-agents27export const BADGE: Record<DesktopDiagnosis["tone"], BadgeTone> = {
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 ok: "success",
29 working: "info",
30 off: "neutral",
31 warning: "warn",
32 problem: "danger",
33};
34
35/** Each kind of job's icon: a terminal for Linux jobs, a window for Windows jobs, a laptop for macOS jobs. */
Merge branch 'main' into rm-agents36export const ICON: Record<string, ReactNode> = {
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.37 linux: <SquareTerminal size={15} aria-hidden />,
38 windows: <AppWindow size={15} aria-hidden />,
39 macos: <Laptop size={15} aria-hidden />,
40};
41
42/** How a kind of job keeps each job from the computer, and what that means. */
43const ISOLATION: Record<string, { label: string; hint: string }> = {
44 "nested-container": { label: "Fresh container in its VM", hint: "Each job runs in a fresh container inside the runner's own Linux VM, and is removed after." },
45 vm: { label: "A VM of its own", hint: "Each job runs in a VM of its own, such as a Windows Sandbox, discarded after." },
46};
47
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.48type Row = {
49 machine: string;
50 person: string | null;
51 backend: DesktopBackend;
52 version: string | null;
53 diagnosis: DesktopDiagnosis;
54 /** This computer's fixes, which only work here. */
55 actions: DesktopRunnerAction[];
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.56 /** Sandboxed: each kind of job, Linux and Windows, with how it is (and here, its fixes and Start or Stop). */
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.57 capabilities: DesktopRunnerCapability[];
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.58 /** The kinds of job say it all: the whole runner's line isn't shown as well. */
59 inCapabilities: boolean;
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.60 here: boolean;
61 reportedAt: string | null;
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.62 /** How its runner's updates stand, while g1t is open there. */
63 update: DesktopUpdate | null;
64 /** Here only: the app's button that looks for a newer runner now. */
65 check: DesktopRunnerAction | null;
Merge branch 'main' into rm-agents66 /** Here only: the app is looking for a newer runner now. */
67 checking: boolean;
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};
69
70/** The desktops to list: this computer, live, first; then the others, as their apps last said, or closed. */
71export function desktopRows(desktops: DesktopRunner[], here: ReturnType<typeof useDesktopRunner>, slug: string): Row[] {
72 const rows: Row[] = [];
73 if (here && here.workspace === slug) {
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.74 const capabilities = here.diagnosis.capabilities ?? [];
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.75 rows.push({
76 machine: here.machine,
77 person: null,
78 backend: here.backend,
79 version: here.version,
80 diagnosis: here.diagnosis,
81 actions: here.diagnosis.actions,
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.82 capabilities,
83 inCapabilities: !!here.diagnosis.in_capabilities && capabilities.length > 0,
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.84 here: true,
85 reportedAt: null,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.86 update: here.diagnosis.update ?? null,
Merge branch 'main' into rm-agents87 check: here.diagnosis.update?.check ?? null,
88 checking: !!here.diagnosis.update?.checking,
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.89 });
90 }
91 for (const desktop of desktops) {
92 if (rows.some((row) => row.here && row.machine.toLowerCase() === desktop.machine.toLowerCase())) continue;
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.93 const capabilities = desktop.open ? (desktop.diagnosis.capabilities ?? []).map((c) => ({ ...c, 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.94 rows.push({
95 machine: desktop.machine,
96 person: desktop.person,
97 backend: desktop.backend,
98 version: desktop.version,
99 diagnosis: desktop.open ? desktop.diagnosis : desktop.closed,
100 actions: [],
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.101 capabilities,
102 inCapabilities: desktop.open && !!desktop.diagnosis.in_capabilities && capabilities.length > 0,
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.103 here: false,
104 reportedAt: desktop.reportedAt,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.105 update: desktop.open ? (desktop.diagnosis.update ?? null) : null,
106 check: null,
Merge branch 'main' into rm-agents107 checking: false,
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.108 });
109 }
110 return rows;
111}
112
Merge branch 'main' into rm-agents113/**
114 * A diagnosis's fixes, as buttons: they work only on this computer, in the
115 * app. Each is busy from its click until the status it waits on changes
116 * (`status`, lib/busy.ts); one the app asks about first asks.
117 */
118export function Actions({
119 actions,
120 primary = true,
121 status,
122 working = false,
123 onError,
124}: {
125 actions: DesktopRunnerAction[];
126 primary?: boolean;
127 /** What a press waits on: the runner's status (`runnerStatusKey`, `kindStatusKey`). */
128 status: string;
129 /** Busy for a reason of the app's own, such as a check already under way. */
130 working?: boolean;
131 onError?: (error: unknown) => void;
132}) {
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.133 if (actions.length === 0) return null;
134 return (
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.135 <>
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.136 {actions.map((action, at) => (
Merge branch 'main' into rm-agents137 <ActionButton key={action.id} action={action} variant={primary && at === 0 ? "secondary" : "outline"} status={status} working={working} onError={onError} />
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.138 ))}
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.139 </>
140 );
141}
142
Merge branch 'main' into rm-agents143function ActionButton({
144 action,
145 variant,
146 status,
147 working,
148 onError,
149}: {
150 action: DesktopRunnerAction;
151 variant: "secondary" | "outline";
152 status: string;
153 working: boolean;
154 onError?: (error: unknown) => void;
155}) {
156 const { busy, run } = useBusy(status, onError);
157 const [asking, setAsking] = useState(false);
158 const go = () => run(() => desktopRunner()?.act(action.id), !!action.waits);
159 // The app's question, as a title and what it means: "Remove this computer's sandbox?" and the rest.
160 const [title, ...rest] = (action.confirm ?? "").split(/(?<=\?)\s+/);
161 return (
162 <>
163 <Button size="xs" variant={variant} busy={busy || working} onClick={() => (action.confirm ? setAsking(true) : go())}>
164 {action.label}
165 </Button>
166 {action.confirm && (
167 <AlertDialog open={asking} onOpenChange={setAsking}>
168 <AlertDialogContent>
169 <AlertDialogHeader>
170 <AlertDialogTitle>{title}</AlertDialogTitle>
171 {rest.length > 0 && <AlertDialogDescription>{rest.join(" ")}</AlertDialogDescription>}
172 </AlertDialogHeader>
173 <AlertDialogFooter>
174 <AlertDialogCancel>Cancel</AlertDialogCancel>
175 <AlertDialogAction onClick={go}>{action.label}</AlertDialogAction>
176 </AlertDialogFooter>
177 </AlertDialogContent>
178 </AlertDialog>
179 )}
180 </>
181 );
182}
183
184/**
185 * Start or Stop for one kind of job on this computer, which leaves the
186 * others as they are: busy from the click until its status says it
187 * started or stopped.
188 */
189export function Toggle({ capability, status, onError }: { capability: DesktopRunnerCapability; status: string; onError?: (error: unknown) => void }) {
190 const { busy, run } = useBusy(status, onError);
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.191 const on = capability.enabled !== false;
192 return (
193 <Button
194 size="xs"
195 variant={on ? "outline" : "soft"}
196 className="ml-auto"
Merge branch 'main' into rm-agents197 busy={busy}
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.198 aria-label={`${on ? "Stop" : "Start"} ${capability.label}`}
Merge branch 'main' into rm-agents199 onClick={() => run(() => desktopRunner()?.act(on ? "stop" : "start", capability.id))}
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.200 >
Merge branch 'main' into rm-agents201 {on ? "Stop" : "Start"}
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.202 </Button>
203 );
204}
205
Merge branch 'main' into rm-agents206/**
207 * A button that asks the runner for something by its id: busy until the
208 * app has it (a page, a window, the log), or with `waits` until the
209 * status that follows changes.
210 */
211export function ActButton({
212 action,
213 label,
214 status,
215 fail,
216 waits = false,
217}: {
218 action: string;
219 label: string;
220 status: string;
221 fail?: (error: unknown) => void;
222 waits?: boolean;
223}) {
224 const { busy, run } = useBusy(status, fail);
225 return (
226 <Button size="xs" variant="outline" busy={busy} onClick={() => run(() => desktopRunner()?.act(action), waits)}>
227 {label}
228 </Button>
229 );
230}
231
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.232function Detail({ term, children }: { term: ReactNode; children: ReactNode }) {
233 return (
234 <div className="flex min-w-0 items-baseline gap-3">
235 <dt className="w-16 shrink-0 text-faint">{term}</dt>
236 <dd className="min-w-0 break-words text-fg-soft">{children}</dd>
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.237 </div>
238 );
239}
240
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.241/** When the app last looked for a newer runner: the app sends when, and this says how long ago, as "Heard from" does. */
242function Checked({ update }: { update: DesktopUpdate | null }) {
243 if (!update?.checked_at) return null;
244 return (
245 <span className="block">
246 Checked <TimeAgo at={update.checked_at} />
247 </span>
248 );
249}
250
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.251/** One kind of job, as a card of its own: its state, what it runs, why it doesn't and the fix, and here its own Start or Stop. */
Merge branch 'main' into rm-agents252export function KindCard({
253 capability,
254 here,
255 runners,
256 update,
257 view = null,
258 onError,
259}: {
260 capability: DesktopRunnerCapability;
261 here: boolean;
262 runners: Runner[];
263 update: DesktopUpdate | null;
264 /** Here: the runner as the app says it now, which its buttons wait on. */
265 view?: DesktopRunnerView | null;
266 onError?: (error: unknown) => void;
267}) {
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.268 const href = jobHref(capability, runners);
269 const isolation = capability.isolation ? (ISOLATION[capability.isolation] ?? { label: capability.isolation, hint: null }) : null;
270 const edge = capability.tone === "problem" ? "border-danger/40" : capability.tone === "warning" ? "border-warn/35" : "";
271 const live = capability.state === "busy" || capability.state === "starting" || capability.state === "downloading" || capability.state === "updating";
272 const fixes = here ? capability.actions : [];
273 const toggle = here && capability.can_toggle;
274 return (
275 <Card tone="plain" radius="lg" className={`flex min-w-0 flex-col gap-3 p-4 ${edge}`} aria-label={capability.label} role="group">
276 <div className="flex items-center gap-3">
277 <span className={`grid size-9 shrink-0 place-items-center rounded-lg bg-raised ${capability.enabled === false ? "text-faint" : "text-fg-soft"}`}>
278 {ICON[capability.id] ?? ICON.linux}
279 </span>
280 <div className="min-w-0">
281 <p className="text-sm font-medium">{capability.label}</p>
282 <Badge tone={BADGE[capability.tone]} className="mt-1" role={here ? "status" : undefined}>
283 <span className={`size-1.5 rounded-full bg-current ${live ? "motion-safe:animate-pulse" : ""}`} aria-hidden />
284 {capability.title}
285 </Badge>
286 </div>
287 </div>
288 {capability.about && <p className="text-sm text-muted">{capability.about}</p>}
289 {capability.job && (
290 <div className="rounded-lg bg-raised px-3 py-2 text-sm">
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.291 <span className="block text-[0.6875rem] font-medium tracking-wide text-muted uppercase">{capability.job.kind === "agent" ? "Sandbox run" : "Running"}</span>
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.292 {href ? (
293 <Link to={href} className="font-medium text-fg underline-offset-4 hover:underline">
294 {capability.job.name}
295 </Link>
296 ) : (
297 <span className="font-medium text-fg">{capability.job.name}</span>
298 )}
299 <span className="text-muted"> in {capability.job.repo}</span>
300 </div>
301 )}
302 {capability.state === "downloading" && typeof capability.progress === "number" && (
303 <div className="flex items-center gap-3">
304 <Progress value={capability.progress} aria-label={`${capability.label}: the sandbox's image`} className="h-1.5" />
305 <span className="w-9 shrink-0 text-right text-xs text-muted tabular-nums">{capability.progress}%</span>
306 </div>
307 )}
308 {(capability.headline || capability.cause || capability.fix) && (
309 <div className="space-y-1 text-sm">
310 {capability.headline && <p className="font-medium text-fg">{capability.headline}</p>}
311 {capability.cause && <p className="text-muted">{capability.cause}</p>}
312 {capability.fix && <p className="text-fg-soft">{capability.fix}</p>}
313 </div>
314 )}
315 {(capability.runner || capability.version || isolation) && (
316 <dl className="space-y-0.5 border-t border-line pt-2 text-xs">
317 {capability.runner && <Detail term="On g1t">{capability.runner}</Detail>}
318 {capability.version && <Detail term="Version">g1t-runner {capability.version}</Detail>}
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.319 {(capability.update || (capability.version && update?.checked_at)) && (
320 <Detail term="Updates">
321 {capability.update && <span className="block text-accent">{capability.update}</span>}
322 <Checked update={update} />
323 </Detail>
324 )}
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.325 {isolation && (
326 <Detail term="Isolation">
327 {isolation.hint ? (
328 <Tooltip>
329 <TooltipTrigger asChild>
330 <span tabIndex={0} className="cursor-help underline decoration-line-strong decoration-dotted underline-offset-4">
331 {isolation.label}
332 </span>
333 </TooltipTrigger>
334 <TooltipContent>{isolation.hint}</TooltipContent>
335 </Tooltip>
336 ) : (
337 isolation.label
338 )}
339 </Detail>
340 )}
341 </dl>
342 )}
343 {(fixes.length > 0 || toggle) && (
344 <div className="mt-auto flex flex-wrap items-center gap-2">
Merge branch 'main' into rm-agents345 <Actions
346 actions={fixes}
347 primary={capability.tone === "problem" || (capability.tone === "warning" && capability.state !== "waiting")}
348 status={kindStatusKey(capability, view)}
349 onError={onError}
350 />
351 {toggle && <Toggle capability={capability} status={kindStatusKey(capability, view)} onError={onError} />}
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.352 </div>
353 )}
354 </Card>
355 );
356}
357
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.358/**
359 * The workspace's desktops: each person's computer with the g1t desktop
360 * app, whose runner runs while g1t is open there. What each says, why it
361 * isn't running and the fix, is the app's own words (lib/desktop-runner.ts).
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.362 * A sandboxed one's kinds of job, Linux and Windows, are cards of their own:
363 * one can run while the other can't, or is stopped; on this computer, each
364 * has its own Start or Stop.
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.365 */
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.366export function DesktopRunners({ desktops, slug, runners = [] }: { desktops: DesktopRunner[] | null; slug: string; runners?: Runner[] }) {
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.367 const here = useDesktopRunner();
368 if (!desktops) return <p className="text-sm text-muted">The desktops can&apos;t be read right now.</p>;
369 const rows = desktopRows(desktops, here, slug);
370 if (rows.length === 0) {
371 return (
372 <EmptyState title="No desktops yet">
373 The <Link to="/download" className="underline underline-offset-2">g1t desktop app</Link> runs this workspace&apos;s self-hosted jobs while it&apos;s open, sandboxed, with nothing to set up.
374 Open it signed in as an owner of {slug}.
375 </EmptyState>
376 );
377 }
378 return (
379 <Card asChild divided>
380 <ul>
381 {rows.map((row) => (
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.382 <li key={row.machine} className="space-y-3 px-4 py-4">
383 <div className="flex items-start gap-3">
384 <div className="min-w-0 grow">
385 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
386 <Laptop size={13} className="text-muted" aria-hidden />
387 <span className="font-medium">{row.machine}</span>
388 {row.here && <Badge tone="accent">This computer</Badge>}
389 <Badge tone={row.backend === "sandboxed" ? "neutral" : "warn"}>{BACKEND_LABEL[row.backend]}</Badge>
390 {row.person && <span className="text-xs text-faint">@{row.person}</span>}
391 </div>
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.392 </div>
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.393 <div className="shrink-0 text-right text-xs text-faint">
394 {row.version && <div>g1t-runner {row.version}</div>}
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.395 {/* With no cards to say it, what becomes of a newer runner, and when the app last looked. */}
396 {row.capabilities.length === 0 && row.update?.note && <div className="mt-0.5 text-accent">{row.update.note}</div>}
397 {row.capabilities.length === 0 && row.update?.checked_at && (
398 <div className="mt-0.5">
399 <Checked update={row.update} />
400 </div>
401 )}
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.402 {row.reportedAt && (
403 <div className="mt-0.5">
404 Heard from <TimeAgo at={row.reportedAt} />
405 </div>
406 )}
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.407 {row.check && (
408 <div className="mt-2">
Merge branch 'main' into rm-agents409 <Actions actions={[row.check]} primary={false} status={here ? runnerStatusKey(here) : ""} working={row.checking} />
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.410 </div>
411 )}
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.412 </div>
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.413 </div>
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.414 {!row.inCapabilities && (
415 <div className="flex items-start gap-3" role={row.here ? "status" : undefined}>
416 <span className={`mt-1.5 size-2 shrink-0 rounded-full ${DOT[row.diagnosis.tone]}`} aria-hidden />
417 <div className="min-w-0">
418 <p className="text-sm text-fg">{row.diagnosis.title}</p>
419 {row.diagnosis.cause && <p className="mt-0.5 text-sm text-muted">{row.diagnosis.cause}</p>}
420 {row.diagnosis.fix && <p className="mt-0.5 text-sm text-fg-soft">{row.diagnosis.fix}</p>}
421 {row.actions.length > 0 && (
422 <div className="mt-2 flex flex-wrap gap-2">
Merge branch 'main' into rm-agents423 <Actions actions={row.actions} status={here ? runnerStatusKey(here) : ""} />
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.424 </div>
425 )}
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.426 </div>
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.427 </div>
428 )}
429 {row.capabilities.length > 0 && (
430 <div className={`grid gap-3 ${row.capabilities.length > 1 ? "sm:grid-cols-2" : ""}`}>
431 {row.capabilities.map((capability) => (
Merge branch 'main' into rm-agents432 <KindCard key={capability.id} capability={capability} here={row.here} runners={runners} update={row.update} view={row.here ? here : 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.433 ))}
434 </div>
435 )}
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.436 </li>
437 ))}
438 </ul>
439 </Card>
440 );
441}

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