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
480 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.

Fixes from the page sweep1/**
2 * A project's or a workspace's secrets and variables, as one list in the
3 * way Vercel lists environment variables: each row is a key, its type
4 * (Secret or Config), the environments it applies to and who reads it.
5 * Adding and editing happen in a side panel, opened by `?add` or
6 * `?edit=<id>` so the page works without scripts.
7 */
8import { Lock, Pencil, Plus, Search, SlidersHorizontal, Trash2, X } from "lucide-react";
9import { useMemo, useState } from "react";
Merge branch 'worktree-agent-a985c01f43538af01'10import { Form, Link, useLocation } from "react-router";
Fixes from the page sweep11
12import type { Setting } from "@g1t/contracts";
13
14import type { SecretsAction, SecretsData } from "../lib/secrets.server";
Merge branch 'worktree-agent-a985c01f43538af01'15import { ButtonLink, EmptyState, ErrorText, SubmitButton, TimeAgo } from "./ui";
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.16import { Badge } 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.17import { Card } from "./ui/card";
Fixes from the page sweep18import { CheckboxOption } from "./ui/checkbox";
Merge branch 'worktree-agent-ae1299e92e4462012'19import { Hint } from "./ui/hint";
Fixes from the page sweep20import { RadioCard, RadioGroup, RadioOption } from "./ui/radio-group";
21import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "./ui/select";
22
23/** The environments every deployment knows; workflow jobs may name others. */
24const KNOWN_ENVIRONMENTS = ["production", "preview"];
25
26const READERS: Record<string, string> = { workflows: "Workflows", deployments: "Deployments" };
27
28function environmentsLabel(environments: string[]): string {
29 if (environments.length === 0) return "All environments";
30 return environments.map((env) => env.charAt(0).toUpperCase() + env.slice(1)).join(", ");
31}
32
33const SELECT =
34 "rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors hover:border-line-strong focus:border-accent-dim";
35
36export function SecretsPanel({
37 data,
38 action,
39 scope,
40 manage,
41}: {
42 data: SecretsData;
43 action: SecretsAction | undefined;
44 scope: "project" | "workspace";
45 manage: boolean;
46}) {
47 const location = useLocation();
48 const params = new URLSearchParams(location.search);
49 const editing = params.get("edit");
50 const adding = params.has("add");
51 const row = editing ? data.rows.find((r) => r.id === editing && r.scope === scope) : undefined;
52 const [query, setQuery] = useState("");
53 const [type, setType] = useState("all");
54 const [environment, setEnvironment] = useState("all");
55 const environments = useMemo(
56 () => [...new Set([...KNOWN_ENVIRONMENTS, ...data.rows.flatMap((r) => r.environments)])],
57 [data.rows],
58 );
59 const shown = data.rows.filter(
60 (r) =>
61 (!query || r.name.toLowerCase().includes(query.toLowerCase()) || r.note?.toLowerCase().includes(query.toLowerCase())) &&
62 (type === "all" || r.kind === type) &&
63 (environment === "all" || r.environments.length === 0 || r.environments.includes(environment)),
64 );
65
66 return (
67 <div className="max-w-5xl">
68 <header className="flex flex-wrap items-start justify-between gap-4">
69 <div>
70 <h2 className="text-lg font-semibold tracking-tight">Secrets and variables</h2>
71 <p className="mt-1 max-w-2xl text-sm text-muted">
72 One list for everything that reads them. Each row says which environments it applies to and whether{" "}
73 <strong className="font-medium text-fg">workflows</strong> (as <code className="text-fg">secrets.KEY</code>{" "}
74 and <code className="text-fg">vars.KEY</code>), <strong className="font-medium text-fg">deployments</strong>{" "}
75 (the build's environment and the running app's <code className="text-fg">env.KEY</code>), or both read it.
76 {scope === "workspace"
77 ? " Every project, or the ones you link, reads the workspace's; a project's own row of the same key wins."
78 : " Rows from the workspace are shown too; adding the same key here replaces them for this project."}{" "}
79 <a href="https://docs.g1t.sh/guides/secrets-and-variables/" className="text-fg hover:underline">
80 How they are read
81 </a>
82 </p>
83 </div>
84 {manage && (
85 <ButtonLink to="?add" variant="accent">
86 <Plus size={14} />
87 Add
88 </ButtonLink>
89 )}
90 </header>
91
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.92 <Card asChild radius="lg" className="mt-4 px-4 py-2.5 text-xs text-muted">
93 <p>
94 Built in: workflows get <code className="text-fg">secrets.G1T_TOKEN</code>, the workspace's own token for
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.95 the run, with <code className="text-fg">secrets.GITHUB_TOKEN</code> as its alias. The merge
96 queue never reads secrets or variables, and runs for people outside the workspace get no secrets.
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.97 </p>
98 </Card>
Fixes from the page sweep99
100 <div className="mt-5 flex flex-wrap gap-2">
101 <label className="relative min-w-56 grow">
102 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
103 <input
104 value={query}
105 onChange={(e) => setQuery(e.target.value)}
106 placeholder="Search keys and notes"
107 aria-label="Search"
108 className={`${SELECT} w-full pl-9`}
109 />
110 </label>
111 <Select value={type} onValueChange={setType}>
112 <SelectTrigger aria-label="Type" className="h-auto w-auto py-2">
113 <SelectValue />
114 </SelectTrigger>
115 <SelectContent align="end">
116 <SelectItem value="all">All types</SelectItem>
117 <SelectSeparator />
118 <SelectItem value="secret" icon={<Lock />}>Secret</SelectItem>
119 <SelectItem value="variable" icon={<SlidersHorizontal />}>Config</SelectItem>
120 </SelectContent>
121 </Select>
122 <Select value={environment} onValueChange={setEnvironment}>
123 <SelectTrigger aria-label="Environment" className="h-auto w-auto py-2">
124 <SelectValue />
125 </SelectTrigger>
126 <SelectContent align="end">
127 <SelectItem value="all">All environments</SelectItem>
128 {environments.length > 0 && <SelectSeparator />}
129 {environments.map((env) => (
130 <SelectItem key={env} value={env}>
131 {environmentsLabel([env])}
132 </SelectItem>
133 ))}
134 </SelectContent>
135 </Select>
136 </div>
137
138 <ErrorText>{data.error}</ErrorText>
139 {!editing && !adding && <div className="mt-2"><ErrorText>{action?.error}</ErrorText></div>}
140
141 <div className="mt-4">
142 {data.rows.length === 0 ? (
143 <EmptyState title="No secrets or variables yet">
144 Add one, or paste a <code>.env</code> file into Add to bring many at once.
145 </EmptyState>
146 ) : shown.length === 0 ? (
147 <EmptyState title="Nothing matches" />
148 ) : (
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.149 <Card asChild className="overflow-hidden">
150 <ul>
151 {shown.map((r) => (
152 <Row key={r.id} row={r} inherited={r.scope !== scope} manage={manage} />
153 ))}
154 </ul>
155 </Card>
Fixes from the page sweep156 )}
157 </div>
158
159 {manage && (adding || row) && (
Merge branch 'worktree-agent-a985c01f43538af01'160 // Keyed to the row, so going from one row's edit to another's starts from that row.
161 <Drawer key={row?.id ?? "add"} row={row} scope={scope} projects={data.projects} error={action?.error} />
Fixes from the page sweep162 )}
163 </div>
164 );
165}
166
167function Row({ row, inherited, manage }: { row: Setting; inherited: boolean; manage: boolean }) {
168 const secret = row.kind === "secret";
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.169 const hosts = row.brokeredHosts ?? [];
Fixes from the page sweep170 return (
171 <li className="grid grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 border-t border-line px-4 py-3 text-sm first:border-t-0 md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)_minmax(0,1fr)_6rem_6rem_auto]">
172 <div className="min-w-0">
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.173 <div className="flex min-w-0 items-center gap-2">
174 <p className="truncate font-mono text-[0.8125rem]">{row.name}</p>
175 {secret && hosts.length > 0 && (
176 <Hint label={`Filled in for ${hosts.join(", ")}`}>
177 <Badge tone="accent">
178 Brokered
179 <span className="sr-only">: filled in for {hosts.join(", ")}</span>
180 </Badge>
181 </Hint>
182 )}
183 </div>
Fixes from the page sweep184 {row.note && <p className="truncate text-xs text-faint">{row.note}</p>}
185 {!secret && row.value != null && <p className="truncate font-mono text-xs text-muted">{row.value}</p>}
186 </div>
187 <span className="truncate text-muted">{environmentsLabel(row.environments)}</span>
188 <span className="hidden truncate text-xs text-muted md:block">
189 {row.availableTo.map((r) => READERS[r] ?? r).join(" · ")}
190 </span>
191 <span className="hidden items-center gap-1.5 text-xs text-muted md:flex">
192 {secret ? <Lock size={13} /> : <SlidersHorizontal size={13} />}
193 {secret ? "Secret" : "Config"}
194 </span>
195 <span className="hidden text-xs text-faint md:block">
196 <TimeAgo at={row.updatedAt} />
197 </span>
198 <span className="flex items-center justify-end gap-1">
199 {inherited ? (
200 <span className="rounded-full px-2 py-px text-xs text-muted ring-1 ring-line">Workspace</span>
201 ) : (
202 <>
203 {row.projects.length > 0 && (
Merge branch 'worktree-agent-ae1299e92e4462012'204 <Hint label={row.projects.join(", ")}>
205 <span className="mr-1 text-xs text-faint">
206 {row.projects.length} {row.projects.length === 1 ? "project" : "projects"}
207 <span className="sr-only">: {row.projects.join(", ")}</span>
208 </span>
209 </Hint>
Fixes from the page sweep210 )}
211 {manage && (
212 <>
213 <Link
214 to={`?edit=${row.id}`}
215 aria-label={`Edit ${row.name}`}
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar216 className="rounded-md p-1.5 text-faint max-sm:p-2.5 transition-colors hover:bg-raised hover:text-fg"
Fixes from the page sweep217 >
218 <Pencil size={14} />
219 </Link>
220 <Form method="post">
221 <input type="hidden" name="intent" value="delete" />
222 <input type="hidden" name="id" value={row.id} />
223 <input type="hidden" name="name" value={row.name} />
Merge branch 'worktree-agent-a985c01f43538af01'224 <SubmitButton
225 icon
226 match={{ intent: "delete", id: row.id }}
Fixes from the page sweep227 aria-label={`Remove ${row.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.228 variant="ghost"
229 size="inline"
230 className="p-1.5 text-faint max-sm:p-2.5 hover:text-danger"
Fixes from the page sweep231 >
232 <Trash2 size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'233 </SubmitButton>
Fixes from the page sweep234 </Form>
235 </>
236 )}
237 </>
238 )}
239 </span>
240 </li>
241 );
242}
243
244function Drawer({
245 row,
246 scope,
247 projects,
248 error,
249}: {
250 row: Setting | undefined;
251 scope: "project" | "workspace";
252 projects: string[];
253 error: string | undefined;
254}) {
255 const editing = !!row;
256 const [type, setType] = useState<"secret" | "config">(row?.kind === "variable" ? "config" : "secret");
257 const [some, setSome] = useState(!!row && row.environments.length > 0);
258 const [reach, setReach] = useState(row && row.projects.length > 0 ? "some" : "all");
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.259 const [brokered, setBrokered] = useState((row?.brokeredHosts?.length ?? 0) > 0);
260 // Only a secret is brokered, and only workflows read one.
261 const brokering = type === "secret" && brokered;
Fixes from the page sweep262 const custom = row?.environments.filter((env) => !KNOWN_ENVIRONMENTS.includes(env)) ?? [];
263 const field =
264 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
265 return (
266 <div className="fixed inset-0 z-50 flex justify-end bg-black/50" role="dialog" aria-modal="true" aria-label={editing ? "Edit" : "Add"}>
267 <Link to="?" aria-label="Close" className="grow" />
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar268 <Form method="post" className="flex h-full w-full max-w-xl flex-col border-l border-line bg-bg pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] shadow-2xl">
Fixes from the page sweep269 <div className="flex items-center justify-between border-b border-line px-6 py-4">
270 <h3 className="font-semibold">{editing ? `Edit ${row.name}` : "Add a secret or variable"}</h3>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar271 <Link to="?" aria-label="Close" className="flex items-center justify-center rounded-md p-1.5 text-faint hover:bg-raised hover:text-fg max-sm:size-10">
Fixes from the page sweep272 <X size={16} />
273 </Link>
274 </div>
275 <div className="grow space-y-6 overflow-y-auto px-6 py-5">
276 <input type="hidden" name="intent" value="save" />
277 {row && <input type="hidden" name="id" value={row.id} />}
278
279 <fieldset>
280 <legend className="mb-2 text-sm font-medium text-muted">Type</legend>
281 <RadioGroup
282 name="type"
283 value={type}
284 onValueChange={(value) => setType(value as typeof type)}
285 aria-label="Type"
286 className="gap-3 sm:grid-cols-2"
287 >
288 {(
289 [
290 ["secret", "Secret", "You can't read it again after saving. For passwords, API keys and tokens.", <Lock key="i" />],
291 ["config", "Config", "Readable by members after saving. For values that are not sensitive.", <SlidersHorizontal key="i" />],
292 ] as const
293 ).map(([value, title, text, icon]) => (
294 // A secret's value is sealed: it can never become config.
295 <RadioCard
296 key={value}
297 value={value}
298 title={title}
299 description={text}
300 icon={icon}
301 disabled={value === "config" && row?.kind === "secret"}
302 />
303 ))}
304 </RadioGroup>
305 {row?.kind === "variable" && (
306 <p className="mt-2 text-xs text-faint">Config can become a secret; a secret cannot become config.</p>
307 )}
308 </fieldset>
309
310 <label className="block">
311 <span className="mb-1.5 block text-sm font-medium text-muted">Key</span>
312 {editing ? (
313 <input name="key" value={row.name} readOnly className={`${field} font-mono text-muted`} />
314 ) : (
315 <textarea
316 name="key"
317 required
318 rows={1}
319 placeholder="CLIENT_KEY, or paste a .env file"
320 autoComplete="off"
321 spellCheck={false}
322 className={`${field} min-h-10 font-mono`}
323 />
324 )}
325 </label>
326
327 <label className="block">
328 <span className="mb-1.5 block text-sm font-medium text-muted">Value</span>
329 <textarea
330 name="value"
331 rows={type === "secret" ? 3 : 2}
332 defaultValue={row?.kind === "variable" ? (row.value ?? "") : ""}
333 placeholder={
334 editing && row.kind === "secret" ? "Leave empty to keep the current value" : "Enter a value"
335 }
336 autoComplete="off"
337 spellCheck={false}
338 className={`${field} font-mono`}
339 />
340 </label>
341
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.342 {type === "secret" && (
343 <fieldset>
344 <legend className="mb-2 text-sm font-medium text-muted">Brokered</legend>
345 <div className="space-y-2 text-sm">
346 <CheckboxOption
347 name="brokered"
348 checked={brokered}
349 onCheckedChange={(checked) => setBrokered(checked === true)}
350 label="Only for these hosts"
351 description="The job gets a stand-in; g1t puts the real value on requests to these hosts, so it never enters the sandbox."
352 />
353 {brokered && (
354 <div className="ml-6 space-y-2">
355 <textarea
356 name="brokeredHosts"
357 rows={2}
358 required
359 defaultValue={row?.brokeredHosts?.join("\n") ?? ""}
360 placeholder={"api.example.com\n*.example.com"}
361 autoComplete="off"
362 spellCheck={false}
363 aria-label="Hosts"
364 className={`${field} font-mono`}
365 />
366 <p className="text-xs text-faint">
367 One per line. Filled in in headers and the query string, for workflow jobs on g1t's machines.{" "}
368 <a href="https://docs.g1t.sh/guides/secrets-and-variables/#brokered-secrets" className="text-fg hover:underline">
369 What it covers
370 </a>
371 </p>
372 </div>
373 )}
374 </div>
375 </fieldset>
376 )}
377
Fixes from the page sweep378 <label className="block">
379 <span className="mb-1.5 block text-sm font-medium text-muted">Note (optional)</span>
380 <input name="note" defaultValue={row?.note ?? ""} placeholder="Where to rotate it, or who to ask" className={field} />
381 </label>
382
383 <fieldset>
384 <legend className="mb-2 text-sm font-medium text-muted">Environments</legend>
385 <div className="space-y-2 text-sm">
386 <RadioGroup name="scope" value={some ? "some" : "all"} onValueChange={(value) => setSome(value === "some")} aria-label="Environments">
387 <RadioOption value="all" label="All environments" />
388 <RadioOption value="some" label="Only some" />
389 </RadioGroup>
390 {some && (
391 <div className="ml-6 space-y-2">
392 {KNOWN_ENVIRONMENTS.map((env) => (
393 <CheckboxOption
394 key={env}
395 name="env"
396 value={env}
397 defaultChecked={row?.environments.includes(env)}
398 label={environmentsLabel([env])}
399 />
400 ))}
401 <input
402 name="envCustom"
403 defaultValue={custom.join(", ")}
404 placeholder="Others, comma-separated: staging, qa"
405 className={field}
406 />
407 <p className="text-xs text-faint">
408 Deployments are production and preview; a workflow job reads the row for its{" "}
409 <code>environment:</code>, and rows for all environments otherwise.
410 </p>
411 </div>
412 )}
413 </div>
414 </fieldset>
415
416 <fieldset>
417 <legend className="mb-2 text-sm font-medium text-muted">Available to</legend>
418 <div className="space-y-2 text-sm">
419 {(
420 [
421 ["workflows", "Workflows", "secrets.KEY or vars.KEY in GitHub Actions workflows"],
422 ["deployments", "Deployments", "The build's environment, and env.KEY in the running app"],
423 ] as const
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.424 ).map(([value, title, text]) => {
425 // A deployment's app needs the value itself, so a brokered
426 // secret is never one of its.
427 const closed = brokering && value === "deployments";
428 return (
429 <CheckboxOption
430 key={`${value}${closed ? "-closed" : ""}`}
431 name="availableTo"
432 value={value}
433 defaultChecked={closed ? false : row ? row.availableTo.includes(value) : true}
434 disabled={closed}
435 label={title}
436 description={closed ? "Not for a brokered secret: a deployment's app needs the value itself." : text}
437 />
438 );
439 })}
Fixes from the page sweep440 </div>
441 </fieldset>
442
443 {scope === "workspace" && (
444 <fieldset>
445 <legend className="mb-2 text-sm font-medium text-muted">Projects</legend>
446 <div className="space-y-2 text-sm">
447 <RadioGroup name="reach" value={reach} onValueChange={(value) => setReach(value as typeof reach)} aria-label="Projects">
448 <RadioOption value="all" label="Every project" />
449 <RadioOption value="some" label="Only these" />
450 </RadioGroup>
451 {reach === "some" && (
452 <div className="ml-6 grid max-h-48 gap-1.5 overflow-y-auto p-0.5 sm:grid-cols-2">
453 {projects.map((name) => (
454 <CheckboxOption
455 key={name}
456 name="project"
457 value={name}
458 defaultChecked={row?.projects.includes(name)}
459 label={name}
460 className="items-center"
461 labelClassName="font-mono text-xs"
462 />
463 ))}
464 </div>
465 )}
466 </div>
467 </fieldset>
468 )}
469 <ErrorText>{error}</ErrorText>
470 </div>
471 <div className="flex items-center justify-between gap-4 border-t border-line px-6 py-4">
472 <p className="text-xs text-faint">{editing ? "" : "Paste .env contents into Key to add many."}</p>
Merge branch 'worktree-agent-a985c01f43538af01'473 <SubmitButton match={{ intent: "save" }} pending="Saving…">
Fixes from the page sweep474 Save
Merge branch 'worktree-agent-a985c01f43538af01'475 </SubmitButton>
Fixes from the page sweep476 </div>
477 </Form>
478 </div>
479 );
480}

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