g1t/apps/web/app/routes/repo/issue.tsx

651 lines27,236 bytesCodeBlame

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.

Issues and pull requests replace intents and attempts1import { env } from "cloudflare:workers";
Fast pages, required checks on the branch, self-hosted runners, honest incidents2import { Bot, ExternalLink, GitCommitHorizontal, GitMerge, Play, Sparkles } from "lucide-react";
Issues and pull requests replace intents and attempts3import { useEffect } from "react";
4import { Form, Link, redirect, useNavigation, useRevalidator } from "react-router";
5
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights6import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts";
Issues and pull requests replace intents and attempts7
8import type { Route } from "./+types/issue";
Social cards for every page: og.g1t.sh9import { excerpt, page } from "../../lib/meta";
Issues and pull requests replace intents and attempts10import { Markdown } from "../../components/markdown";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains11import { AgentStepLine } from "../../components/agents";
Issues and pull requests replace intents and attempts12import {
13 Avatar,
14 Button,
15 CopyLine,
16 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17 ComputeNote,
Issues and pull requests replace intents and attempts18 ErrorText,
19 Input,
20 Textarea,
21 TimeAgo,
22} from "../../components/ui";
Workspace names and icons, and a component kit for every control23import { CheckboxOption } from "../../components/ui/checkbox";
Acceptance checks in sandboxes, line comments and review verdicts24import { CheckBadge } from "../../components/checks";
Agents as a team: lifecycle, merge queue, billing and a new shell25import {
26 Assignee,
27 AssigneeStack,
28 ChangeSize,
29 CommentForm,
30 CommentList,
31 PeoplePicker,
32 IssueState,
33 Label,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains34 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell35 PullIcon,
36 plainText,
37} from "../../components/work";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put38import { notFound } from "../../lib/not-found.server";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent39import { openedBy } from "../../lib/opened-by";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look40import { computeNoteFor } from "../../lib/compute.server";
Integrations: your own model provider, alerts that open issues, tickets agents read41import { identity, integrations, work } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell42import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look43import { accessTo, refusal } from "../../lib/access.server";
Issues and pull requests replace intents and attempts44
45const REFRESH_MS = 4000;
46
Social cards for every page: og.g1t.sh47export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
48 const issue = loaderData?.issue;
49 const title = issue ? `${issue.title} · Issue #${issue.number} · ` : "";
50 const state = issue?.state === "open" ? "Open" : issue?.reason === "not_planned" ? "Closed as not planned" : "Closed";
51 const body = excerpt(issue?.body);
52 return page(args, {
53 title: `${title}${params.owner}/${params.repo} · g1t`,
54 description: issue
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights55 ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}${issue.requestedBy ? ` for ${issue.requestedBy.username}` : ""}.${body ? ` ${body}` : ""}`
Social cards for every page: og.g1t.sh56 : null,
57 // The card shows the title and the state.
58 version: issue ? [issue.title, issue.state, issue.reason] : undefined,
59 type: "article",
60 });
Issues and pull requests replace intents and attempts61}
62
63export async function loader({ params, context }: Route.LoaderArgs) {
64 const viewer = getViewer(context);
65 const path = { namespace: params.owner, name: params.repo };
66 const number = Number(params.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents67 // At once: only the plan's note waits for the viewer's role. Putting an
68 // agent on it needs Write: Read cannot spend compute.
69 const access = accessTo(context, params);
70 const [{ can }, found, labels, agentsEnabled, members, links, computeNote] = await Promise.all([
71 access,
Agents as a team: lifecycle, merge queue, billing and a new shell72 work.getIssue(path, number, viewer),
73 work.listLabels(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing74 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell75 // A member picks assignees from the workspace's people.
76 roleIn(viewer, params.owner) ? identity.listMembers(params.owner, viewer) : null,
Integrations: your own model provider, alerts that open issues, tickets agents read77 // What it is tied to outside g1t. Shown only once the issue is known visible.
78 integrations.links(path, number).catch(() => []),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent79 // Before a member assigns g1t: whether the workspace's plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents80 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
Agents as a team: lifecycle, merge queue, billing and a new shell81 ]);
Issues and pull requests replace intents and attempts82 if (!found.ok) {
83 // Issues and pull requests share numbers; this one may be a pull request.
84 const pull = await work.getPull(path, number, viewer);
85 if (pull.ok) throw redirect(`/${params.owner}/${params.repo}/pull/${number}`);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put86 throw notFound("issue");
Issues and pull requests replace intents and attempts87 }
88 const { issue } = found.value;
89 return {
90 ...found.value,
91 viewer,
92 labels: labels.ok ? labels.value : [],
Agents as a team: lifecycle, merge queue, billing and a new shell93 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look94 computeNote,
Integrations: your own model provider, alerts that open issues, tickets agents read95 links,
Agents as a team: lifecycle, merge queue, billing and a new shell96 members: members?.ok ? members.value.map((member) => member.username) : [],
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights97 // The author can close and reopen their own issue, and whoever g1t's
98 // agent filed one for, that one; Triage and up, anyone's.
99 canManage: viewer != null && (viewer.id === workOwner(issue).id || can.triage),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look100 can,
Issues and pull requests replace intents and attempts101 };
102}
103
104export async function action({ request, params, context }: Route.ActionArgs) {
105 assertSameOrigin(request);
106 const user = requireUser(context, request);
107 const form = await request.formData();
108 const path = { namespace: params.owner, name: params.repo };
109 const number = Number(params.number);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look110 // What each form needs; closing your own issue is checked by work.
111 const needed = form.get("action") === "run-hosted" ? "run" : form.get("action") === "assign" || form.get("action") === "labels" ? "triage" : null;
112 const refused = needed ? await refusal(context, params, needed) : null;
113 if (refused) return { error: refused, action: String(form.get("action")) };
Issues and pull requests replace intents and attempts114
115 switch (form.get("action")) {
116 case "run-hosted": {
117 const result = await env.RUNNER.run(user, path, number, {
118 instructions: String(form.get("instructions") ?? ""),
119 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look120 return result.ok ? null : { error: result.error.message, action: "run-hosted" };
Issues and pull requests replace intents and attempts121 }
122 case "open-pull": {
123 const result = await work.openPull(user, path, {
124 issue: number,
125 agent: String(form.get("agent") ?? ""),
126 runtime: "external",
127 });
128 if (!result.ok) return { error: result.error.message };
129 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
130 }
131 case "comment": {
Acceptance checks in sandboxes, line comments and review verdicts132 const result = await work.addComment(user, path, number, {
133 body: String(form.get("body") ?? ""),
134 });
Issues and pull requests replace intents and attempts135 return result.ok ? null : { error: result.error.message };
136 }
Agents as a team: lifecycle, merge queue, billing and a new shell137 case "assign": {
138 const result = await work.updateIssue(user, path, number, {
139 assignees: [
140 ...form.getAll("assignee").map(String),
141 ...String(form.get("others") ?? "").split(/[\s,]+/),
142 ],
143 });
144 return result.ok ? null : { error: result.error.message };
145 }
Issues and pull requests replace intents and attempts146 case "labels": {
147 const result = await work.updateIssue(user, path, number, {
148 labels: [
149 ...form.getAll("label").map(String),
150 ...String(form.get("labels") ?? "").split(","),
151 ],
152 });
153 return result.ok ? null : { error: result.error.message };
154 }
155 case "reopen": {
156 const result = await work.reopenIssue(user, path, number);
157 return result.ok ? null : { error: result.error.message };
158 }
159 default: {
160 const result = await work.closeIssue(
161 user,
162 path,
163 number,
164 form.get("action") === "close-not-planned" ? "not_planned" : "completed",
165 );
166 return result.ok ? null : { error: result.error.message };
167 }
168 }
169}
170
171/** What became of a pull request, in a few words. */
172function outcome(pull: Pull): string {
173 if (pull.status === "merged") return `Merged by ${pull.mergedBy ?? "someone"}`;
174 if (pull.supersededBy != null) return `Closed · #${pull.supersededBy} was merged instead`;
175 if (pull.status === "closed") return "Closed without merging";
176 if (pull.status === "draft") return "Draft · in progress";
177 return "Ready for review";
178}
179
180function PullRow({ pull, base }: { pull: Pull; base: string }) {
181 const merged = pull.status === "merged";
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent182 const opener = openedBy(pull);
Issues and pull requests replace intents and attempts183 return (
184 <li>
185 <Link
186 to={`${base}/pull/${pull.number}`}
187 className={`block rounded-xl border p-4 transition-colors ${
188 merged
189 ? "border-merged/40 bg-merged/5 hover:border-merged/70"
190 : "border-line bg-surface hover:border-line-strong"
191 } ${pull.status === "closed" ? "opacity-70" : ""}`}
192 >
193 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
194 <PullIcon status={pull.status} />
195 <span className="font-medium">
196 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
197 </span>
198 {pull.status === "draft" && (
199 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
200 )}
201 <span className="ml-auto flex items-center gap-3 text-xs text-faint">
Agents as a team: lifecycle, merge queue, billing and a new shell202 <ChangeSize files={pull.files} />
Acceptance checks in sandboxes, line comments and review verdicts203 <CheckBadge status={pull.checkStatus} />
Issues and pull requests replace intents and attempts204 <span className="flex items-center gap-1 font-mono">
205 <GitCommitHorizontal size={13} />
206 {pull.headCommit?.slice(0, 7) ?? "no commits"}
207 </span>
208 <TimeAgo at={pull.updatedAt} />
209 </span>
210 </div>
211 <p className="mt-1.5 flex flex-wrap items-center gap-x-2 text-xs text-muted">
212 <span className={merged ? "font-medium text-merged" : ""}>{outcome(pull)}</span>
213 <span className="text-faint">·</span>
214 <span className="flex items-center gap-1 font-mono">
215 <Bot size={12} />
216 {pull.agent}
217 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent218 {opener.requestedBy ? (
219 <span className="text-faint">· requested by {opener.requestedBy}</span>
220 ) : (
221 <>
222 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
223 <span className="text-faint">· opened by {opener.name}</span>
224 </>
225 )}
Issues and pull requests replace intents and attempts226 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell227 {pull.body && (
228 <p className="mt-2 line-clamp-2 text-sm text-muted">{plainText(pull.body)}</p>
229 )}
Issues and pull requests replace intents and attempts230 </Link>
231 </li>
232 );
233}
234
235export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData;
Issues and pull requests replace intents and attempts237
238 // Follow agents at work without a manual reload.
239 const revalidator = useRevalidator();
240 const navigation = useNavigation();
Acceptance checks in sandboxes, line comments and review verdicts241 const running = pulls.some(
242 (pull) =>
243 pull.status === "draft" ||
244 pull.checkStatus === "queued" ||
245 pull.checkStatus === "running",
246 );
Agents as a team: lifecycle, merge queue, billing and a new shell247 // The pull request g1t's agent has in progress for this issue, if any.
248 const assigned = [...pulls]
249 .reverse()
250 .find(
251 (pull) =>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent252 pull.agent === "g1t" &&
Agents as a team: lifecycle, merge queue, billing and a new shell253 pull.runtime === "hosted" &&
254 (pull.status === "draft" || pull.status === "open"),
255 );
Issues and pull requests replace intents and attempts256 useEffect(() => {
Agents as a team: lifecycle, merge queue, billing and a new shell257 if (!running && !assigned) return;
Issues and pull requests replace intents and attempts258 const timer = setInterval(() => {
259 if (document.visibilityState === "visible") revalidator.revalidate();
260 }, REFRESH_MS);
261 return () => clearInterval(timer);
Agents as a team: lifecycle, merge queue, billing and a new shell262 }, [running, assigned, revalidator]);
Issues and pull requests replace intents and attempts263
264 const starting = navigation.formData?.get("action") === "run-hosted";
265 const base = `/${params.owner}/${params.repo}`;
266 const open = issue.state === "open";
267 const reference = `${params.owner}/${params.repo}#${issue.number}`;
268 const resolver = pulls.find((pull) => pull.number === issue.resolvedBy);
269 // The merged pull request first, then the ones still in play, then the rest.
270 const rank = { merged: 0, open: 1, draft: 2, closed: 3 } as const;
271 const ordered = [...pulls].sort(
272 (a, b) => rank[a.status] - rank[b.status] || a.number - b.number,
273 );
274
275 return (
276 <div className="grid gap-8 lg:grid-cols-[1fr_19rem]">
277 <div className="min-w-0">
278 <h2 className="text-2xl font-semibold tracking-tight text-balance">
279 {issue.title} <span className="font-normal text-faint">#{issue.number}</span>
280 </h2>
281 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
282 <IssueState issue={issue} />
283 <span className="flex items-center gap-2">
284 <Avatar name={issue.author.username} size={18} />
285 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains286 <PersonLink name={issue.author.username} className="font-medium text-fg hover:underline" /> opened this{" "}
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights287 {issue.requestedBy && (
288 <>
289 for{" "}
290 <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "}
291 </>
292 )}
Issues and pull requests replace intents and attempts293 <TimeAgo at={issue.createdAt} />
294 </span>
295 </span>
296 {issue.labels.map((name) => (
297 <Label key={name} name={name} />
298 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell299 {open && issue.agent && <Assignee agent={issue.agent} />}
Issues and pull requests replace intents and attempts300 </div>
301
302 {issue.resolvedBy != null && (
303 <Link
304 to={`${base}/pull/${issue.resolvedBy}`}
305 className="mt-5 flex items-center gap-3 rounded-xl border border-merged/40 bg-merged/5 px-4 py-3 text-sm transition-colors hover:border-merged/70"
306 >
307 <GitMerge size={18} className="shrink-0 text-merged" />
308 <span>
309 Resolved by{" "}
310 <span className="font-medium">
311 {resolver?.title ?? "pull request"} #{issue.resolvedBy}
312 </span>
313 {resolver?.mergedBy && (
314 <span className="text-muted">
315 , merged by {resolver.mergedBy}{" "}
316 {resolver.mergedAt && <TimeAgo at={resolver.mergedAt} />}
317 </span>
318 )}
319 </span>
320 </Link>
321 )}
322
323 {issue.body && (
324 <div className="mt-5 rounded-xl border border-line bg-surface p-5">
Agents as a team: lifecycle, merge queue, billing and a new shell325 <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} />
Issues and pull requests replace intents and attempts326 </div>
327 )}
328
329 <div className="mt-10 flex items-baseline justify-between">
330 <h3 className="font-semibold tracking-tight">Pull requests</h3>
331 <p className="text-sm text-muted">
332 {pulls.length === 0
333 ? "None yet"
334 : `${pulls.length} for this issue${
335 pulls.some((pull) => pull.status === "merged") ? "" : ", none merged"
336 }`}
337 </p>
338 </div>
339 <div className="mt-3">
340 {pulls.length === 0 ? (
341 <EmptyState title="Nobody has worked on this yet">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent342 Put g1t on it, or point your own agent at{" "}
Issues and pull requests replace intents and attempts343 <code className="font-mono">{reference}</code>.
344 </EmptyState>
345 ) : (
346 <ol className="space-y-3">
347 {ordered.map((pull) => (
348 <PullRow key={pull.id} pull={pull} base={base} />
349 ))}
350 </ol>
351 )}
352 </div>
353
354 <h3 className="mt-10 font-semibold tracking-tight">Discussion</h3>
355 <div className="mt-3">
Agents as a team: lifecycle, merge queue, billing and a new shell356 <div className="space-y-4">
357 <CommentList comments={comments} base={base} />
358 <CommentForm author={viewer?.username ?? null} resetKey={comments.length} />
359 {canManage && (
360 <Form method="post" className="flex flex-wrap justify-end gap-2">
361 {open ? (
362 <>
363 <Button variant="quiet" type="submit" name="action" value="close-not-planned">
364 Close as not planned
365 </Button>
366 <Button variant="quiet" type="submit" name="action" value="close-completed">
367 Close issue
368 </Button>
369 </>
370 ) : (
371 <Button variant="quiet" type="submit" name="action" value="reopen">
372 Reopen issue
373 </Button>
374 )}
375 </Form>
376 )}
377 </div>
Issues and pull requests replace intents and attempts378 </div>
379 <div className="mt-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look380 {!(actionData && "action" in actionData) && <ErrorText>{actionData?.error}</ErrorText>}
Issues and pull requests replace intents and attempts381 </div>
382 </div>
383
384 <aside className="space-y-6">
Integrations: your own model provider, alerts that open issues, tickets agents read385 {loaderData.links.length > 0 && (
386 <section>
387 <h3 className="text-sm font-medium">From outside g1t</h3>
388 <ul className="mt-2 space-y-1.5 text-sm">
389 {loaderData.links.map((link) => (
390 <li key={`${link.connectionId}-${link.key}`}>
391 <a
392 href={link.url}
393 target="_blank"
394 rel="noreferrer"
395 className="group flex items-center gap-2 rounded-lg border border-line px-2.5 py-2 transition-colors hover:border-line-strong"
396 >
397 <span className="min-w-0 grow">
398 <span className="block truncate font-medium">
399 {PROVIDERS[link.provider].label} <span className="font-mono text-muted">{link.key}</span>
400 </span>
401 <span className="block text-xs text-faint">
402 {link.count > 1 ? `Seen ${link.count} times, last ` : "Linked "}
403 <TimeAgo at={link.count > 1 ? link.lastSeen : link.firstSeen} />
404 </span>
405 </span>
406 <ExternalLink size={13} className="shrink-0 text-faint group-hover:text-fg" />
407 </a>
408 </li>
409 ))}
410 </ul>
411 </section>
412 )}
Agents as a team: lifecycle, merge queue, billing and a new shell413 <section>
414 <h3 className="text-sm font-medium">Assignees</h3>
415 <ul className="mt-2 space-y-1.5 text-sm">
416 {open && assigned && (
417 <li>
418 <Link
419 to={`${base}/pull/${assigned.number}`}
420 className="flex items-center gap-2 rounded-lg border border-accent/30 bg-accent/5 px-2.5 py-2 transition-colors hover:border-accent/60"
421 >
422 <Sparkles size={15} className="shrink-0 text-accent" />
423 <span className="min-w-0 grow">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent424 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell425 <span className="block truncate text-xs text-muted">
426 {assigned.status === "draft"
427 ? "Making the change"
428 : "Seeing it through checks and review"}{" "}
429 · #{assigned.number}
430 </span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains431 <AgentStepLine owner={params.owner} repo={params.repo} number={assigned.number} />
Agents as a team: lifecycle, merge queue, billing and a new shell432 </span>
433 <span className="size-1.5 shrink-0 animate-pulse rounded-full bg-accent" />
434 </Link>
435 </li>
436 )}
437 {issue.assignees.map((name) => (
438 <li key={name} className="flex items-center gap-2 px-1">
439 <Avatar name={name} size={20} />
440 <span className="grow truncate font-mono text-xs">{name}</span>
441 </li>
442 ))}
443 {open && issue.queued && !assigned && (
444 <li className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2">
445 <Sparkles size={15} className="shrink-0 text-faint" />
446 <span className="min-w-0 grow">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent447 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell448 <span className="block text-xs text-muted">
449 {issue.blockedBy.length > 0
450 ? "Queued. Starts when what this depends on has merged."
451 : "Queued. Starts as soon as there is room."}
452 </span>
453 </span>
454 </li>
455 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent456 {!open && resolver && resolver.agent === "g1t" && (
Agents as a team: lifecycle, merge queue, billing and a new shell457 <li>
458 <Link
459 to={`${base}/pull/${resolver.number}`}
460 className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2 transition-colors hover:border-line-strong"
461 >
462 <Sparkles size={15} className="shrink-0 text-merged" />
463 <span className="min-w-0 grow">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent464 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell465 <span className="block truncate text-xs text-muted">
466 Resolved it with #{resolver.number}
467 </span>
468 </span>
469 </Link>
470 </li>
471 )}
472 {issue.assignees.length === 0 &&
473 !(open && (assigned || issue.queued)) &&
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent474 !(!open && resolver?.agent === "g1t") && (
Agents as a team: lifecycle, merge queue, billing and a new shell475 <li className="px-1 text-xs text-faint">No one yet.</li>
476 )}
477 </ul>
478 {issue.blockedBy.length > 0 && (
479 <p className="mt-3 text-xs text-muted">
480 Depends on{" "}
481 {issue.blockedBy.map((number, index) => (
482 <span key={number}>
483 {index > 0 && ", "}
484 <Link
485 to={`${base}/issues/${number}`}
486 className="font-medium text-fg hover:underline"
487 >
488 #{number}
489 </Link>
490 </span>
491 ))}
492 {open ? ", which has to merge first." : "."}
Issues and pull requests replace intents and attempts493 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell494 )}
495
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look496 {open && agentsEnabled && can.run && !assigned && !issue.queued && (
Issues and pull requests replace intents and attempts497 <Form method="post" className="mt-3 space-y-2">
498 <input type="hidden" name="action" value="run-hosted" />
499 <div className="*:w-full">
500 <Button variant="accent" type="submit" disabled={starting}>
Agents as a team: lifecycle, merge queue, billing and a new shell501 <Sparkles size={14} />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent502 {starting ? "Starting a sandbox…" : "Assign to g1t"}
Issues and pull requests replace intents and attempts503 </Button>
504 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell505 <details>
506 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
507 Add guidance for this run
508 </summary>
509 <div className="mt-2">
510 <Textarea
511 name="instructions"
512 rows={2}
513 placeholder="On top of the issue's description"
514 />
515 </div>
516 </details>
517 <p className="text-xs text-muted">
518 It opens a pull request and sees it through checks, a review by another
519 agent and fixes. You get it back ready to merge.
520 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look521 <ComputeNote note={loaderData.computeNote} />
522 {actionData && "action" in actionData && actionData.action === "run-hosted" && (
523 <ErrorText>{actionData.error}</ErrorText>
524 )}
Issues and pull requests replace intents and attempts525 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell526 )}
527
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look528 {open && !agentsEnabled && can.run && !assigned && !issue.queued && (
New workspaces are told to connect a model, before anything fails529 // Where g1t's agent would be, and what makes it appear.
530 <div className="mt-3 rounded-lg border border-dashed border-line p-3 text-sm">
531 <p className="flex items-center gap-1.5 font-medium">
532 <Sparkles size={14} className="text-accent" />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent533 g1t
New workspaces are told to connect a model, before anything fails534 </p>
535 <p className="mt-1 text-xs text-muted">
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent536 Connect a model provider and g1t can take this issue: it opens a pull request and sees it
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look537 through checks, review and fixes. Agents run on a paid workspace, or on the free trial.
New workspaces are told to connect a model, before anything fails538 </p>
539 <Link
540 to={`/${params.owner}/-/integrations`}
541 className="mt-2 inline-block text-xs text-accent hover:underline"
542 >
543 Connect a model
544 </Link>
545 </div>
546 )}
547
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look548 {viewer && can.triage && open && (
Agents as a team: lifecycle, merge queue, billing and a new shell549 <div className="mt-3 space-y-2">
550 {!issue.assignees.includes(viewer.username) && (
551 <Form method="post">
552 <input type="hidden" name="action" value="assign" />
553 {issue.assignees.map((name) => (
554 <input key={name} type="hidden" name="assignee" value={name} />
555 ))}
556 <input type="hidden" name="assignee" value={viewer.username} />
557 <div className="*:w-full">
558 <Button variant="quiet" type="submit">
559 Assign yourself
560 </Button>
561 </div>
562 </Form>
563 )}
564 <details className="group">
565 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
566 Assign people
567 </summary>
568 <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}>
569 <input type="hidden" name="action" value="assign" />
570 <PeoplePicker name="assignee" members={members} chosen={issue.assignees} />
571 <Button variant="quiet" type="submit">
572 Save assignees
573 </Button>
574 </Form>
575 </details>
576 </div>
577 )}
578 </section>
Issues and pull requests replace intents and attempts579
580 {open && (
581 <section className="rounded-xl border border-line bg-surface p-4">
582 <h3 className="text-sm font-medium">Bring your own agent</h3>
583 <p className="mt-1 text-xs text-muted">
584 With g1t connected to your agent, ask it to work on this issue.
585 </p>
586 <div className="mt-3">
587 <CopyLine text={reference} />
588 </div>
589 {viewer ? (
590 <Form method="post" className="mt-4 space-y-2 border-t border-line pt-4">
591 <input type="hidden" name="action" value="open-pull" />
592 <p className="text-xs text-muted">
593 Or open a draft pull request yourself and get a fork to push to.
594 </p>
595 <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} />
596 <div className="*:w-full">
597 <Button type="submit">Open pull request</Button>
598 </div>
Pull requests from branches599 <p className="text-xs text-muted">
600 Already pushed a branch?{" "}
601 <Link
602 to={`${base}/pulls/new?issue=${issue.number}`}
603 className="text-fg underline underline-offset-4"
604 >
605 Open a pull request from it
606 </Link>
607 .
608 </p>
Issues and pull requests replace intents and attempts609 </Form>
610 ) : (
611 <p className="mt-4 border-t border-line pt-4 text-sm text-muted">
612 <Link to="/login" className="text-fg underline underline-offset-4">
613 Sign in
614 </Link>{" "}
615 to open a pull request.
616 </p>
617 )}
618 </section>
619 )}
620
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look621 {can.triage && (
Issues and pull requests replace intents and attempts622 <details className="group">
623 <summary className="cursor-pointer list-none text-sm font-medium">
624 Labels <span className="text-xs font-normal text-faint group-open:hidden">Edit</span>
625 </summary>
626 <Form method="post" className="mt-3 space-y-3" key={issue.labels.join()}>
627 <input type="hidden" name="action" value="labels" />
628 <div className="flex flex-wrap gap-x-3 gap-y-2">
629 {labels.map((name) => (
Workspace names and icons, and a component kit for every control630 <CheckboxOption
631 key={name}
632 name="label"
633 value={name}
634 defaultChecked={issue.labels.includes(name)}
635 label={<Label name={name} />}
636 className="items-center gap-1.5"
637 />
Issues and pull requests replace intents and attempts638 ))}
639 </div>
640 <Input name="labels" placeholder="New labels, comma separated" />
641 <Button variant="quiet" type="submit">
642 Save labels
643 </Button>
644 </Form>
645 </details>
646 )}
647
648 </aside>
649 </div>
650 );
651}