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

Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.1import { AtSign, Mail, UserPlus } from "lucide-react";
2import { type ReactNode, useEffect, useId, useRef, useState } from "react";
3import { useFetcher } from "react-router";
4
5import { MAX_INVITE_MESSAGE, type PersonMatch } from "@g1t/contracts";
6
7import { ErrorText, SubmitButton } from "./ui";
8import { Avatar } from "./ui/avatar";
9import { Button } from "./ui/button";
10import { Command, CommandInput, CommandItem, CommandList } from "./ui/command";
11import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
12import { Field, FieldDescription, FieldLabel } from "./ui/field";
13import { SelectField } from "./ui/select";
14import { Textarea } from "./ui/textarea";
15import { UserCard } from "./user-card";
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.16import { type InviteResult, workspaceInviteCopy } from "../lib/invites";
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.17import { PEOPLE_SEARCH_PATH, looksLikeEmail, peopleQuery } from "../lib/people-search";
18
19/**
20 * Inviting someone to the workspace, in a dialog opened from Members and
21 * from Invitations: who (a username, a name to search by, or an email
22 * address), the role they join with, and a note for the email. Posted to
23 * `action` as `action=add` (lib/member-invitations.server.ts); on success the
24 * dialog closes and `onInvited` is told who, so the page can say so.
25 */
26export function InviteDialog({
27 name,
28 action,
29 onInvited,
30 trigger,
31}: {
32 /** The workspace's display name. */
33 name: string;
34 /** The page's address, whose action handles `add`. */
35 action: string;
36 onInvited?: (who: string) => void;
37 /** The button that opens it; the Invite button when absent. */
38 trigger?: ReactNode;
39}) {
40 const [open, setOpen] = useState(false);
41 const fetcher = useFetcher<InviteResult>();
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.42 const copy = workspaceInviteCopy(name);
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.43 const result = fetcher.state === "idle" ? fetcher.data : undefined;
44 // Sent: the dialog closes and the page says so. Each opening starts clean.
45 const told = useRef<InviteResult | null>(null);
46 useEffect(() => {
47 if (!result || told.current === result) return;
48 told.current = result;
49 if ("invited" in result) {
50 setOpen(false);
51 onInvited?.(result.invited);
52 }
53 }, [result, onInvited]);
54 const error = result && "error" in result ? result : null;
55 const messageId = useId();
56 return (
57 <Dialog open={open} onOpenChange={setOpen}>
58 <DialogTrigger asChild>
59 {trigger ?? (
60 <Button type="button">
61 <UserPlus size={15} />
62 Invite
63 </Button>
64 )}
65 </DialogTrigger>
66 <DialogContent>
67 {open && (
68 <fetcher.Form method="post" action={action} className="grid gap-5">
69 <input type="hidden" name="action" value="add" />
70 <DialogHeader>
71 <DialogTitle>{copy.heading}</DialogTitle>
72 <DialogDescription>{copy.hint}</DialogDescription>
73 </DialogHeader>
74 <WhoField />
75 <Field>
76 <FieldLabel htmlFor={`${messageId}-role`}>Role</FieldLabel>
77 <SelectField
78 id={`${messageId}-role`}
79 name="role"
80 defaultValue="member"
81 options={[
82 { value: "member", label: "Member", description: `Works in ${name} with the base permission on its repositories.` },
83 { value: "owner", label: "Owner", description: "Everything a member can, and manages the workspace itself." },
84 ]}
85 />
86 </Field>
87 <Field>
88 <FieldLabel htmlFor={messageId}>
89 Message <span className="font-normal text-faint">(optional)</span>
90 </FieldLabel>
91 <Textarea id={messageId} name="message" maxLength={MAX_INVITE_MESSAGE} rows={3} placeholder={`A line for the email: why you'd like them in ${name}.`} />
92 <FieldDescription>Quoted in the invitation email, under your name.</FieldDescription>
93 </Field>
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.94 {error && <ErrorText>{error.error}</ErrorText>}
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.95 <DialogFooter>
96 <DialogClose asChild>
97 <Button type="button" variant="outline">
98 Cancel
99 </Button>
100 </DialogClose>
101 <SubmitButton fetcher={fetcher} match={{ action: "add" }} pending="Inviting…">
102 <UserPlus size={15} />
103 Invite
104 </SubmitButton>
105 </DialogFooter>
106 </fetcher.Form>
107 )}
108 </DialogContent>
109 </Dialog>
110 );
111}
112
113/**
114 * Who to invite: a search box that offers people on g1t by username and
115 * name as you type, each with their picture and card, and takes an email
116 * address as written. Only what a profile shows is offered, never anyone's
117 * address. Posts what is in the box as `member`.
118 */
119function WhoField() {
120 const [text, setText] = useState("");
121 const [people, setPeople] = useState<PersonMatch[]>([]);
122 const [chosen, setChosen] = useState<string | null>(null);
123 const asked = useRef(0);
124 const id = useId();
125
126 useEffect(() => {
127 const query = chosen === text.trim().replace(/^@+/, "") ? null : peopleQuery(text);
128 if (!query) {
129 setPeople([]);
130 return;
131 }
132 const ask = ++asked.current;
133 const timer = setTimeout(() => {
134 fetch(`${PEOPLE_SEARCH_PATH}?q=${encodeURIComponent(query)}`, { headers: { accept: "application/json" } })
135 .then((response) => (response.ok ? (response.json() as Promise<{ people?: PersonMatch[] }>) : { people: [] }))
136 .then((found) => {
137 if (ask === asked.current) setPeople(found.people ?? []);
138 })
139 .catch(() => {
140 if (ask === asked.current) setPeople([]);
141 });
142 }, 150);
143 return () => clearTimeout(timer);
144 }, [text, chosen]);
145
146 const email = looksLikeEmail(text);
147 const listed = people.length > 0 && !email;
148 return (
149 <Field>
150 <FieldLabel htmlFor={id}>Who</FieldLabel>
151 <Command shouldFilter={false} className="rounded-md border border-line bg-bg has-[input:focus-visible]:border-accent-dim has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-accent/25">
152 <CommandInput
153 id={id}
154 name="member"
155 required
156 maxLength={254}
157 autoComplete="off"
158 placeholder="username, name or name@example.com"
159 value={text}
160 onValueChange={(value) => {
161 setText(value);
162 setChosen(null);
163 }}
164 className="h-9"
165 />
166 {listed && (
167 <CommandList aria-label="People on g1t" className="max-h-56 border-t border-line">
168 {people.map((person) => (
169 <CommandItem
170 key={person.username}
171 value={person.username}
172 onSelect={() => {
173 setText(person.username);
174 setChosen(person.username);
175 setPeople([]);
176 }}
177 >
178 <Avatar name={person.username} image={person.avatar} size={20} />
179 <UserCard username={person.username}>
180 <span className="font-mono">{person.username}</span>
181 </UserCard>
182 {person.name && <span className="min-w-0 truncate text-muted">{person.name}</span>}
183 </CommandItem>
184 ))}
185 </CommandList>
186 )}
187 </Command>
188 <FieldDescription className="flex items-center gap-1.5">
189 {email ? (
190 <>
191 <Mail size={12} className="shrink-0" />
192 Invited by email: {text.trim()} gets the invitation, and signs up first if it has no account.
193 </>
194 ) : chosen ? (
195 <>
196 <AtSign size={12} className="shrink-0" />
197 @{chosen} gets the invitation in their notifications and by email.
198 </>
199 ) : (
200 "People on g1t are offered as you type; an email address is sent as written."
201 )}
202 </FieldDescription>
203 </Field>
204 );
205}

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