Skip to content
541 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.

Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1import { Bot, Boxes, Cpu, Plus, ServerCog, ShieldAlert, Trash2 } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { useEffect, useState } from "react";
3import { Form, Link } from "react-router";
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays4
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.5import { DESKTOP_LABEL, type RegistrationToken, RUNNER_DOWNLOADS, RUNNER_FILES, RUNNER_IMAGE, type Runner, type RunnerGroup, type RunnerIsolation } from "@g1t/contracts";
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays6
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.7import { agentRunHref, workflowRunHref } from "../lib/runners";
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays8import type { RunnersAction, RunnersData } from "../lib/runners.server";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.9import { CopyLine, EmptyState, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.10import { Badge, type BadgeTone } from "./ui/badge";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.11import { Button } from "./ui/button";
12import { Card } from "./ui/card";
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.13import { Checkbox } from "./ui/checkbox";
14import { RadioGroup, RadioOption } from "./ui/radio-group";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project15import { SelectField } from "./ui/select";
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays16import { Switch } from "./ui/switch";
17import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.18import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays19
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.20/** Online and waiting for work reads as Idle; online and working, Busy. */
21export const STATUS: Record<Runner["status"], { dot: string; text: string; label: string }> = {
22 online: { dot: "bg-success", text: "text-success", label: "Idle" },
23 busy: { dot: "bg-info", text: "text-info", label: "Busy" },
24 offline: { dot: "bg-faint", text: "text-muted", label: "Offline" },
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays25};
26
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.27/** Where a runner's current work is: its workflow run, or the agent run it is. */
28function workHref(work: NonNullable<Runner["work"]>): string | null {
29 if (!work.runId || !work.repo) return null;
30 return work.kind === "workflow" ? workflowRunHref(work.repo, work.runId) : agentRunHref(work.repo, work.runId);
31}
32
33export const OS_NAMES: Record<Runner["os"], string> = { linux: "Linux", macos: "macOS", windows: "Windows" };
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays34
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.35/** How a runner keeps work from its machine, as a badge and what it means. */
36export const ISOLATION: Record<RunnerIsolation, { label: string; tone: BadgeTone; about: string }> = {
37 "nested-container": {
38 label: "Sandboxed",
39 tone: "success",
40 about: "Each job runs in a fresh container inside the runner's own sandboxed container. Neither reaches the machine.",
41 },
42 vm: {
43 label: "VM per job",
44 tone: "success",
45 about: "Each job runs in a throwaway virtual machine of its own, such as Windows Sandbox, with no folder of the machine's.",
46 },
47 container: {
48 label: "Shared Docker",
49 tone: "warn",
50 about: "Each job runs in a container, but the runner shares the machine's Docker Engine, so anything that takes it over controls the machine.",
51 },
52 host: {
53 label: "Unsandboxed",
54 tone: "danger",
55 about: "Jobs run directly on the machine (host mode). It is not given public repositories' work.",
56 },
57};
58
59function IsolationBadge({ isolation }: { isolation: Runner["isolation"] }) {
60 if (!isolation) return null;
61 const level = ISOLATION[isolation];
62 if (!level) return null;
63 return (
64 <Tooltip>
65 <TooltipTrigger asChild>
66 <Badge size="md" tone={level.tone} tabIndex={0}>
67 {level.label}
68 </Badge>
69 </TooltipTrigger>
70 <TooltipContent className="max-w-72">{level.about}</TooltipContent>
71 </Tooltip>
72 );
73}
74
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays75function Section({ icon, title, about, children }: { icon: React.ReactNode; title: string; about: React.ReactNode; children: React.ReactNode }) {
76 return (
77 <section className="grid gap-x-10 gap-y-4 border-t border-line pt-8 first:border-t-0 first:pt-0 lg:grid-cols-[16rem_1fr]">
78 <div>
79 <h2 className="flex items-center gap-2 font-medium">
80 {icon}
81 {title}
82 </h2>
83 <div className="mt-1 text-sm text-muted">{about}</div>
84 </div>
85 <div className="min-w-0">{children}</div>
86 </section>
87 );
88}
89
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.90export function RunnerRow({ runner, manage }: { runner: Runner; manage: boolean }) {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays91 const status = STATUS[runner.status];
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.92 const href = runner.work ? workHref(runner.work) : null;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays93 return (
94 <li className="flex flex-wrap items-start gap-x-4 gap-y-2 px-4 py-3">
95 <span className={`mt-1.5 size-2 shrink-0 rounded-full ${status.dot}`} aria-hidden />
96 <div className="min-w-0 grow basis-60">
97 <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
98 <span className="font-medium">{runner.name}</span>
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.99 <span className={`text-xs font-medium ${status.text}`}>
100 {/* A desktop app's runner runs while g1t is open there: offline is g1t closed. */}
101 {runner.status === "offline" && runner.labels.includes(DESKTOP_LABEL) ? `g1t is closed on ${runner.name}` : status.label}
102 </span>
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.103 <IsolationBadge isolation={runner.isolation} />
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.104 {runner.ephemeral && <Badge size="md">ephemeral</Badge>}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays105 {runner.group && <span className="text-xs text-faint">in {runner.group}</span>}
106 </div>
107 <div className="mt-1.5 flex flex-wrap gap-1">
108 {runner.labels.map((label) => (
109 <span key={label} className="rounded-md bg-raised px-1.5 py-0.5 font-mono text-[0.6875rem] text-fg-soft">
110 {label}
111 </span>
112 ))}
113 </div>
114 {runner.work && (
115 <p className="mt-1.5 truncate text-xs text-muted">
116 {runner.work.kind === "workflow" ? "Running " : "Agent work: "}
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.117 {href ? (
118 <Link to={href} className="text-fg hover:underline">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays119 {runner.work.name}
120 </Link>
121 ) : (
122 <span className="text-fg">{runner.work.name}</span>
123 )}
124 {runner.work.repo && <> in {runner.work.repo}</>}
125 </p>
126 )}
127 </div>
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.128 <div className="ml-6 grow text-xs text-faint sm:ml-0 sm:shrink-0 sm:grow-0 sm:text-right">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays129 <div>
130 {OS_NAMES[runner.os]} {runner.arch}
131 {runner.version && <> · {runner.version}</>}
132 </div>
133 <div className="mt-0.5">{runner.lastSeenAt ? <>Seen <TimeAgo at={runner.lastSeenAt} /></> : "Never seen"}</div>
134 </div>
135 {manage && (
136 <Form
137 method="post"
138 className="shrink-0"
139 onSubmit={(event) => {
140 if (!confirm(`Remove ${runner.name}? Its credential stops working at once${runner.work ? ", and the job it is running fails" : ""}.`)) event.preventDefault();
141 }}
142 >
143 <input type="hidden" name="intent" value="remove" />
144 <input type="hidden" name="id" value={runner.id} />
145 <input type="hidden" name="name" value={runner.name} />
Merge branch 'worktree-agent-a985c01f43538af01'146 <SubmitButton
147 icon
148 match={{ intent: "remove", id: runner.id }}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays149 aria-label={`Remove ${runner.name}`}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.150 variant="ghost"
151 size="inline"
152 className="p-1.5 text-faint hover:text-danger"
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays153 >
154 <Trash2 size={15} />
Merge branch 'worktree-agent-a985c01f43538af01'155 </SubmitButton>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays156 </Form>
157 )}
158 </li>
159 );
160}
161
162type Platform = "linux" | "macos" | "windows" | "docker";
163
164/** The copy-paste steps for one platform. */
165export function installSteps(platform: Platform, token: RegistrationToken | null, arch: "x64" | "arm64" = "x64"): { title: string; lines: string[] }[] {
166 const url = token?.url ?? "https://g1t.sh";
167 const value = token?.token ?? "<registration token>";
168 const register = (exe: string) => `${exe} register --url ${url} --token ${value}`;
169 switch (platform) {
170 case "linux": {
171 const file = arch === "arm64" ? RUNNER_FILES["linux-arm64"] : RUNNER_FILES["linux-x64"];
172 return [
173 { title: "Download", lines: [`curl -fsSLo g1t-runner ${RUNNER_DOWNLOADS}/latest/${file} && chmod +x g1t-runner`] },
174 { title: "Register", lines: [register("./g1t-runner")] },
175 { title: "Run it, or install it as a service", lines: ["./g1t-runner run", "sudo ./g1t-runner service install"] },
176 ];
177 }
178 case "macos": {
179 const file = arch === "x64" ? RUNNER_FILES["macos-x64"] : RUNNER_FILES["macos-arm64"];
180 return [
181 { title: "Download", lines: [`curl -fsSLo g1t-runner ${RUNNER_DOWNLOADS}/latest/${file} && chmod +x g1t-runner`] },
182 { title: "Register", lines: [register("./g1t-runner")] },
183 { title: "Run it, or install it as a launch agent", lines: ["./g1t-runner run", "./g1t-runner service install"] },
184 ];
185 }
186 case "windows":
187 return [
188 {
189 title: "Download (PowerShell)",
190 lines: [`Invoke-WebRequest ${RUNNER_DOWNLOADS}/latest/${RUNNER_FILES["windows-x64"]} -OutFile g1t-runner.exe`],
191 },
192 { title: "Register", lines: [register(".\\g1t-runner.exe")] },
193 { title: "Run it, or install it as a service (as Administrator)", lines: [".\\g1t-runner.exe run", ".\\g1t-runner.exe service install"] },
194 ];
195 case "docker":
196 return [
197 {
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9198 title: "Register and run under Sysbox, each job in a container inside the runner's",
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays199 lines: [
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9200 `docker run -d --name g1t-runner --restart unless-stopped --runtime=sysbox-runc -v g1t-runner:/data ${RUNNER_IMAGE} register-and-run --url ${url} --token ${value}`,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays201 ],
202 },
203 ];
204 }
205}
206
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.207export function NewRunner({ token, groups, repoScoped }: { token: RegistrationToken | null; groups: RunnerGroup[]; repoScoped: boolean }) {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays208 const [platform, setPlatform] = useState<Platform>("linux");
209 const [arch, setArch] = useState<"x64" | "arm64">("x64");
210 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.211 <Card className="space-y-4 p-4">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays212 {!token ? (
213 <Form method="post" className="flex flex-wrap items-end gap-3">
214 <input type="hidden" name="intent" value="token" />
215 {!repoScoped && groups.length > 1 && (
216 <Field label="Group">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project217 <SelectField
218 name="group"
219 defaultValue={groups.find((g) => g.default)?.name ?? groups[0]?.name}
220 className="w-auto min-w-40"
221 options={groups.map((group) => ({ value: group.name, label: group.name }))}
222 />
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays223 </Field>
224 )}
Merge branch 'worktree-agent-a985c01f43538af01'225 <SubmitButton match={{ intent: "token" }} pending="Making a token…">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays226 <Plus size={15} />
227 New runner
Merge branch 'worktree-agent-a985c01f43538af01'228 </SubmitButton>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays229 <p className="basis-full text-xs text-faint">Makes a registration token that lasts an hour and registers any number of runners. It is shown once.</p>
230 </Form>
231 ) : (
232 <p className="text-sm">
233 Registration token made. It expires <TimeAgoFuture at={token.expiresAt} />
234 {token.group ? <>; its runners join {token.group}</> : null}. Copy it now: it is not shown again.
235 </p>
236 )}
237 <Tabs value={platform} onValueChange={(value) => setPlatform(value as Platform)}>
238 <div className="flex flex-wrap items-center gap-3">
239 <TabsList aria-label="Where the runner runs">
240 <TabsTrigger value="linux">Linux</TabsTrigger>
241 <TabsTrigger value="macos">macOS</TabsTrigger>
242 <TabsTrigger value="windows">Windows</TabsTrigger>
243 <TabsTrigger value="docker">Docker</TabsTrigger>
244 </TabsList>
245 {(platform === "linux" || platform === "macos") && (
246 <TabsList aria-label="Architecture">
247 <button
248 type="button"
249 onClick={() => setArch("x64")}
250 className={`rounded-md px-2.5 py-1 text-xs font-medium ${arch === "x64" ? "bg-raised text-fg" : "text-muted hover:text-fg"}`}
251 >
252 {platform === "macos" ? "Intel" : "x64"}
253 </button>
254 <button
255 type="button"
256 onClick={() => setArch("arm64")}
257 className={`rounded-md px-2.5 py-1 text-xs font-medium ${arch === "arm64" ? "bg-raised text-fg" : "text-muted hover:text-fg"}`}
258 >
259 {platform === "macos" ? "Apple silicon" : "arm64"}
260 </button>
261 </TabsList>
262 )}
263 </div>
264 </Tabs>
265 <ol className="space-y-3">
266 {installSteps(platform, token, platform === "macos" && arch === "x64" ? "x64" : arch).map((step, at) => (
267 <li key={step.title}>
268 <p className="mb-1.5 text-xs text-muted">
269 {at + 1}. {step.title}
270 </p>
271 <div className="space-y-1.5">
272 {step.lines.map((line) => (
273 <CopyLine key={line} text={line} prompt />
274 ))}
275 </div>
276 </li>
277 ))}
278 </ol>
279 <p className="text-xs text-faint">
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9280 Each job runs in a container of its own; on Linux, run the Docker command under Sysbox so neither the runner nor its jobs reach the machine.{" "}
281 <code className="font-mono">--no-docker</code>, which runs jobs on the machine itself, is deprecated. Add{" "}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays282 <code className="font-mono">--labels gpu,cuda</code> for jobs to ask for, or <code className="font-mono">--ephemeral</code> for a runner that takes
283 one job and removes itself.{" "}
284 <a href="https://docs.g1t.sh/guides/self-hosted-runners/" className="underline underline-offset-2">
285 Self-hosted runners
286 </a>
287 </p>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.288 </Card>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays289 );
290}
291
292/** "in 59m", for a time still to come. */
293function TimeAgoFuture({ at }: { at: string }) {
294 const minutes = Math.max(0, Math.round((new Date(at).getTime() - Date.now()) / 60_000));
295 return (
296 <time dateTime={at} suppressHydrationWarning>
297 {minutes >= 60 ? "in an hour" : `in ${minutes}m`}
298 </time>
299 );
300}
301
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.302export function Groups({
Merge branch 'worktree-agent-a985c01f43538af01'303 groups,
304 repositories,
305 manage,
306 action,
307}: {
308 groups: RunnerGroup[];
309 repositories: string[];
310 manage: boolean;
311 action: RunnersAction | undefined;
312}) {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays313 const [editing, setEditing] = useState<string | null>(null);
Merge branch 'worktree-agent-a985c01f43538af01'314 // A group saved closes its form: the list above now shows it as saved.
315 useEffect(() => {
316 if (action?.notice) setEditing(null);
317 }, [action]);
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays318 return (
319 <div className="space-y-3">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.320 <Card asChild divided>
321 <ul>
322 {groups.map((group) => (
323 <li key={group.id} className="px-4 py-3">
324 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
325 <span className="font-medium">{group.name}</span>
326 {group.default && <Badge size="md">default</Badge>}
327 <span className="text-xs text-muted">
328 {group.runners} {group.runners === 1 ? "runner" : "runners"} ·{" "}
329 {group.repositories.length === 0 ? "every repository" : group.repositories.join(", ")}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays330 </span>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.331 {manage && (
332 <span className="ml-auto flex items-center gap-2">
333 <Button type="button" variant="outline" onClick={() => setEditing(editing === group.id ? null : group.id)}>
334 {editing === group.id ? "Close" : "Edit"}
335 </Button>
336 {!group.default && (
337 <Form method="post" onSubmit={(event) => { if (!confirm(`Delete ${group.name}? Its runners join the default group.`)) event.preventDefault(); }}>
338 <input type="hidden" name="intent" value="delete-group" />
339 <input type="hidden" name="id" value={group.id} />
340 <SubmitButton variant="outline" match={{ intent: "delete-group", id: group.id }} pending="Deleting…">
341 Delete
342 </SubmitButton>
343 </Form>
344 )}
345 </span>
346 )}
347 </div>
348 {editing === group.id && <GroupForm group={group} repositories={repositories} />}
349 </li>
350 ))}
351 </ul>
352 </Card>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays353 {manage && (editing === "new" ? <GroupForm group={null} repositories={repositories} /> : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.354 <Button type="button" variant="outline" onClick={() => setEditing("new")}>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays355 <Plus size={15} />
356 New group
357 </Button>
358 ))}
359 </div>
360 );
361}
362
363function GroupForm({ group, repositories }: { group: RunnerGroup | null; repositories: string[] }) {
364 const [reach, setReach] = useState(group && group.repositories.length > 0 ? "some" : "all");
365 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.366 <Card asChild tone="bg" radius="lg" className="mt-3 space-y-3 p-3">
367 <Form method="post">
368 <input type="hidden" name="intent" value="group" />
369 {group && <input type="hidden" name="id" value={group.id} />}
370 <Field label="Name">
371 <Input name="name" defaultValue={group?.name ?? ""} placeholder="GPU machines" maxLength={64} required={!group} />
372 </Field>
373 <fieldset className="space-y-2 text-sm">
374 <legend className="mb-1 text-xs text-muted">Repositories that may use its runners</legend>
375 <RadioGroup name="reach" value={reach} onValueChange={setReach}>
376 <RadioOption value="all" label="Every repository in the workspace" />
377 <RadioOption value="some" label="Only these" />
378 </RadioGroup>
379 {reach === "some" && (
380 <div className="grid gap-1 pl-6 sm:grid-cols-2">
381 {repositories.map((name) => (
382 <label key={name} className="flex min-h-8 cursor-pointer items-center gap-2 font-mono text-xs">
383 <Checkbox name="repository" value={name} defaultChecked={group?.repositories.includes(name)} />
384 {name}
385 </label>
386 ))}
387 </div>
388 )}
389 </fieldset>
390 <SubmitButton match={{ intent: "group", id: group?.id }} pending="Saving…">
391 Save group
392 </SubmitButton>
393 </Form>
394 </Card>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays395 );
396}
397
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.398export function RunnerSettingsForm({ data, manage, scope }: { data: RunnersData; manage: boolean; scope: "workspace" | "project" }) {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays399 const settings = data.settings;
400 const [agents, setAgents] = useState(settings?.agentsOnSelfHosted ?? false);
401 const [forks, setForks] = useState(settings?.forkPullRequests ?? false);
402 if (!settings) return null;
Merge branch 'worktree-agent-a985c01f43538af01'403 // The intent comes from the button pressed: a hidden one before it would
404 // win the form's first "intent", and Follow the workspace would only save.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays405 return (
406 <Form method="post" className="space-y-4">
407 {scope === "project" && settings.inherited && (
408 <p className="text-sm text-muted">These are the workspace&apos;s settings. Saving here gives this project its own.</p>
409 )}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.410 <Card asChild className="flex items-start justify-between gap-4 p-4">
411 <label>
412 <span className="min-w-0">
413 <span className="flex items-center gap-2 text-sm font-medium">
414 <Bot size={15} className="text-muted" />
415 Run g1t's work on self-hosted runners
416 </span>
417 <span className="mt-1 block text-sm text-muted">
418 Agent runs, checks, reviews and the merge queue run on your runners with these labels instead of g1t&apos;s sandboxes. The machine time is
419 free; the model is paid as before, through g1t&apos;s model proxy, or by your own provider if you connected one. Agent work needs Docker, or a
420 Linux runner.
421 </span>
422 <span className="mt-3 block max-w-sm">
423 <Input name="agentLabels" defaultValue={settings.agentLabels.join(", ")} disabled={!manage} aria-label="Labels for agent work" />
424 </span>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays425 </span>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.426 <Switch name="agents" checked={agents} onCheckedChange={setAgents} disabled={!manage} aria-label="Run g1t's work on self-hosted runners" />
427 </label>
428 </Card>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays429 <label className={`flex items-start justify-between gap-4 rounded-xl border p-4 ${forks ? "border-danger/50 bg-danger/5" : "border-line bg-surface"}`}>
430 <span className="min-w-0">
431 <span className="flex items-center gap-2 text-sm font-medium">
432 <ShieldAlert size={15} className={forks ? "text-danger" : "text-muted"} />
433 Let pull requests from forks use self-hosted runners
434 </span>
435 <span className="mt-1 block text-sm text-muted">
436 Off by default, and best left off. Anyone who can open a pull request could then run any code on your machines, read what they can
437 reach, and leave something behind for the next job. Their jobs never get secrets either way.
438 </span>
439 </span>
440 <Switch name="forks" checked={forks} onCheckedChange={setForks} disabled={!manage} />
441 </label>
442 <p className="text-xs text-faint">
443 g1t&apos;s network guardrails are not enforced on your own machines: a job or an agent there reaches whatever the machine can.
444 </p>
445 {manage && (
446 <div className="flex flex-wrap gap-3">
Merge branch 'worktree-agent-a985c01f43538af01'447 <SubmitButton name="intent" value="settings" pending="Saving…">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays448 Save
Merge branch 'worktree-agent-a985c01f43538af01'449 </SubmitButton>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays450 {scope === "project" && !settings.inherited && (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.451 <SubmitButton name="intent" value="inherit" variant="outline" pending="Following…">
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays452 Follow the workspace
Merge branch 'worktree-agent-a985c01f43538af01'453 </SubmitButton>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays454 )}
455 </div>
456 )}
457 </Form>
458 );
459}
460
461/** Settings, Runners: a workspace's, or a project's own. */
462export function RunnersPanel({
463 data,
464 action,
465 scope,
466 manage,
467}: {
468 data: RunnersData;
469 action: RunnersAction | undefined;
470 scope: "workspace" | "project";
471 manage: boolean;
472}) {
473 const online = data.runners.filter((runner) => runner.status !== "offline").length;
474 return (
475 <div className="max-w-5xl space-y-8">
476 <div className="min-h-6">
Merge branch 'worktree-agent-ad7c6d88d93adc817'477 {action?.notice && <p className="text-sm text-success">{action.notice}</p>}
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays478 <ErrorText>{action?.error ?? data.error ?? null}</ErrorText>
479 </div>
480 <Section
481 icon={<ServerCog size={15} className="text-muted" />}
482 title="Runners"
483 about={
484 <>
485 <p>
486 Your own machines, for jobs with <code className="font-mono text-fg">runs-on: self-hosted</code>. Their time is free on every plan.
487 </p>
488 <p className="mt-2">
489 {data.runners.length === 0 ? "None yet." : `${online} of ${data.runners.length} online.`}
490 </p>
491 </>
492 }
493 >
494 {data.runners.length === 0 ? (
495 <EmptyState title="No self-hosted runners">
496 Add one below. It connects out to g1t; nothing needs to reach the machine.
497 </EmptyState>
498 ) : (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.499 <Card asChild divided>
500 <ul>
501 {data.runners.map((runner) => (
502 <RunnerRow key={runner.id} runner={runner} manage={manage && (scope === "workspace" || runner.repo !== null)} />
503 ))}
504 </ul>
505 </Card>
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays506 )}
507 </Section>
508 {manage && (
509 <Section
510 icon={<Plus size={15} className="text-muted" />}
511 title="New runner"
512 about="Download g1t-runner, register it with a token made here, and start it. Linux, macOS and Windows, x64 and arm64."
513 >
514 <NewRunner token={action?.token ?? null} groups={data.groups} repoScoped={scope === "project"} />
515 </Section>
516 )}
517 {scope === "workspace" && (
518 <Section
519 icon={<Boxes size={15} className="text-muted" />}
520 title="Groups"
521 about="Which repositories may use which runners. A runner joins the default group, every repository, unless its token names another."
522 >
Merge branch 'worktree-agent-a985c01f43538af01'523 <Groups groups={data.groups} repositories={data.repositories} manage={manage} action={action} />
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays524 </Section>
525 )}
526 <Section
527 icon={<Cpu size={15} className="text-muted" />}
528 title="Where work runs"
529 about={scope === "workspace" ? "For every project in the workspace, unless a project says otherwise." : "For this project."}
530 >
Merge branch 'worktree-agent-a985c01f43538af01'531 {/* Keyed to what is saved, so following the workspace again shows its settings, not the switches as they were. */}
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.532 <RunnerSettingsForm
Merge branch 'worktree-agent-a985c01f43538af01'533 key={data.settings ? `${data.settings.inherited}:${data.settings.agentsOnSelfHosted}:${data.settings.forkPullRequests}:${data.settings.agentLabels.join(",")}` : "none"}
534 data={data}
535 manage={manage}
536 scope={scope}
537 />
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays538 </Section>
539 </div>
540 );
541}

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