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

643 lines26,950 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";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending3import { Form, Link, redirect, useNavigation } from "react-router";
Issues and pull requests replace intents and attempts4
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights5import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts";
Issues and pull requests replace intents and attempts6
7import type { Route } from "./+types/issue";
Social cards for every page: og.g1t.sh8import { excerpt, page } from "../../lib/meta";
Issues and pull requests replace intents and attempts9import { Markdown } from "../../components/markdown";
Agents and memory, checks and conflicts, profiles, slug renames, custom domains10import { AgentStepLine } from "../../components/agents";
Issues and pull requests replace intents and attempts11import {
12 Avatar,
13 Button,
14 CopyLine,
15 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look16 ComputeNote,
Issues and pull requests replace intents and attempts17 ErrorText,
18 Input,
19 Textarea,
20 TimeAgo,
21} from "../../components/ui";
Workspace names and icons, and a component kit for every control22import { CheckboxOption } from "../../components/ui/checkbox";
Acceptance checks in sandboxes, line comments and review verdicts23import { CheckBadge } from "../../components/checks";
Agents as a team: lifecycle, merge queue, billing and a new shell24import {
25 Assignee,
26 AssigneeStack,
27 ChangeSize,
28 CommentForm,
29 CommentList,
30 PeoplePicker,
31 IssueState,
32 Label,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell34 PullIcon,
35 plainText,
36} from "../../components/work";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put37import { 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-agent38import { openedBy } from "../../lib/opened-by";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look39import { computeNoteFor } from "../../lib/compute.server";
Integrations: your own model provider, alerts that open issues, tickets agents read40import { identity, integrations, work } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell41import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42import { accessTo, refusal } from "../../lib/access.server";
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending43import { useRefreshWhile } from "../../lib/refresh";
Issues and pull requests replace intents and attempts44
45
Social cards for every page: og.g1t.sh46export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
47 const issue = loaderData?.issue;
48 const title = issue ? `${issue.title} · Issue #${issue.number} · ` : "";
49 const state = issue?.state === "open" ? "Open" : issue?.reason === "not_planned" ? "Closed as not planned" : "Closed";
50 const body = excerpt(issue?.body);
51 return page(args, {
52 title: `${title}${params.owner}/${params.repo} · g1t`,
53 description: issue
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights54 ? `${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.sh55 : null,
56 // The card shows the title and the state.
57 version: issue ? [issue.title, issue.state, issue.reason] : undefined,
58 type: "article",
59 });
Issues and pull requests replace intents and attempts60}
61
62export async function loader({ params, context }: Route.LoaderArgs) {
63 const viewer = getViewer(context);
64 const path = { namespace: params.owner, name: params.repo };
65 const number = Number(params.number);
Fast pages, required checks on the branch, self-hosted runners, honest incidents66 // At once: only the plan's note waits for the viewer's role. Putting an
67 // agent on it needs Write: Read cannot spend compute.
68 const access = accessTo(context, params);
69 const [{ can }, found, labels, agentsEnabled, members, links, computeNote] = await Promise.all([
70 access,
Agents as a team: lifecycle, merge queue, billing and a new shell71 work.getIssue(path, number, viewer),
72 work.listLabels(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing73 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell74 // A member picks assignees from the workspace's people.
75 roleIn(viewer, params.owner) ? identity.listMembers(params.owner, viewer) : null,
Integrations: your own model provider, alerts that open issues, tickets agents read76 // What it is tied to outside g1t. Shown only once the issue is known visible.
77 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-agent78 // Before a member assigns g1t: whether the workspace's plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents79 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
Agents as a team: lifecycle, merge queue, billing and a new shell80 ]);
Issues and pull requests replace intents and attempts81 if (!found.ok) {
82 // Issues and pull requests share numbers; this one may be a pull request.
83 const pull = await work.getPull(path, number, viewer);
84 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 put85 throw notFound("issue");
Issues and pull requests replace intents and attempts86 }
87 const { issue } = found.value;
88 return {
89 ...found.value,
90 viewer,
91 labels: labels.ok ? labels.value : [],
Agents as a team: lifecycle, merge queue, billing and a new shell92 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 computeNote,
Integrations: your own model provider, alerts that open issues, tickets agents read94 links,
Agents as a team: lifecycle, merge queue, billing and a new shell95 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 rights96 // The author can close and reopen their own issue, and whoever g1t's
97 // agent filed one for, that one; Triage and up, anyone's.
98 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 look99 can,
Issues and pull requests replace intents and attempts100 };
101}
102
103export async function action({ request, params, context }: Route.ActionArgs) {
104 assertSameOrigin(request);
105 const user = requireUser(context, request);
106 const form = await request.formData();
107 const path = { namespace: params.owner, name: params.repo };
108 const number = Number(params.number);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 // What each form needs; closing your own issue is checked by work.
110 const needed = form.get("action") === "run-hosted" ? "run" : form.get("action") === "assign" || form.get("action") === "labels" ? "triage" : null;
111 const refused = needed ? await refusal(context, params, needed) : null;
112 if (refused) return { error: refused, action: String(form.get("action")) };
Issues and pull requests replace intents and attempts113
114 switch (form.get("action")) {
115 case "run-hosted": {
116 const result = await env.RUNNER.run(user, path, number, {
117 instructions: String(form.get("instructions") ?? ""),
118 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 return result.ok ? null : { error: result.error.message, action: "run-hosted" };
Issues and pull requests replace intents and attempts120 }
121 case "open-pull": {
122 const result = await work.openPull(user, path, {
123 issue: number,
124 agent: String(form.get("agent") ?? ""),
125 runtime: "external",
126 });
127 if (!result.ok) return { error: result.error.message };
128 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
129 }
130 case "comment": {
Acceptance checks in sandboxes, line comments and review verdicts131 const result = await work.addComment(user, path, number, {
132 body: String(form.get("body") ?? ""),
133 });
Issues and pull requests replace intents and attempts134 return result.ok ? null : { error: result.error.message };
135 }
Agents as a team: lifecycle, merge queue, billing and a new shell136 case "assign": {
137 const result = await work.updateIssue(user, path, number, {
138 assignees: [
139 ...form.getAll("assignee").map(String),
140 ...String(form.get("others") ?? "").split(/[\s,]+/),
141 ],
142 });
143 return result.ok ? null : { error: result.error.message };
144 }
Issues and pull requests replace intents and attempts145 case "labels": {
146 const result = await work.updateIssue(user, path, number, {
147 labels: [
148 ...form.getAll("label").map(String),
149 ...String(form.get("labels") ?? "").split(","),
150 ],
151 });
152 return result.ok ? null : { error: result.error.message };
153 }
154 case "reopen": {
155 const result = await work.reopenIssue(user, path, number);
156 return result.ok ? null : { error: result.error.message };
157 }
158 default: {
159 const result = await work.closeIssue(
160 user,
161 path,
162 number,
163 form.get("action") === "close-not-planned" ? "not_planned" : "completed",
164 );
165 return result.ok ? null : { error: result.error.message };
166 }
167 }
168}
169
170/** What became of a pull request, in a few words. */
171function outcome(pull: Pull): string {
172 if (pull.status === "merged") return `Merged by ${pull.mergedBy ?? "someone"}`;
173 if (pull.supersededBy != null) return `Closed · #${pull.supersededBy} was merged instead`;
174 if (pull.status === "closed") return "Closed without merging";
175 if (pull.status === "draft") return "Draft · in progress";
176 return "Ready for review";
177}
178
179function PullRow({ pull, base }: { pull: Pull; base: string }) {
180 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-agent181 const opener = openedBy(pull);
Issues and pull requests replace intents and attempts182 return (
183 <li>
184 <Link
185 to={`${base}/pull/${pull.number}`}
186 className={`block rounded-xl border p-4 transition-colors ${
187 merged
188 ? "border-merged/40 bg-merged/5 hover:border-merged/70"
189 : "border-line bg-surface hover:border-line-strong"
190 } ${pull.status === "closed" ? "opacity-70" : ""}`}
191 >
192 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
193 <PullIcon status={pull.status} />
194 <span className="font-medium">
195 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
196 </span>
197 {pull.status === "draft" && (
198 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
199 )}
200 <span className="ml-auto flex items-center gap-3 text-xs text-faint">
Agents as a team: lifecycle, merge queue, billing and a new shell201 <ChangeSize files={pull.files} />
Acceptance checks in sandboxes, line comments and review verdicts202 <CheckBadge status={pull.checkStatus} />
Issues and pull requests replace intents and attempts203 <span className="flex items-center gap-1 font-mono">
204 <GitCommitHorizontal size={13} />
205 {pull.headCommit?.slice(0, 7) ?? "no commits"}
206 </span>
207 <TimeAgo at={pull.updatedAt} />
208 </span>
209 </div>
210 <p className="mt-1.5 flex flex-wrap items-center gap-x-2 text-xs text-muted">
211 <span className={merged ? "font-medium text-merged" : ""}>{outcome(pull)}</span>
212 <span className="text-faint">·</span>
213 <span className="flex items-center gap-1 font-mono">
214 <Bot size={12} />
215 {pull.agent}
216 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent217 {opener.requestedBy ? (
218 <span className="text-faint">· requested by {opener.requestedBy}</span>
219 ) : (
220 <>
221 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
222 <span className="text-faint">· opened by {opener.name}</span>
223 </>
224 )}
Issues and pull requests replace intents and attempts225 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell226 {pull.body && (
227 <p className="mt-2 line-clamp-2 text-sm text-muted">{plainText(pull.body)}</p>
228 )}
Issues and pull requests replace intents and attempts229 </Link>
230 </li>
231 );
232}
233
234export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235 const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData;
Issues and pull requests replace intents and attempts236
237 // Follow agents at work without a manual reload.
238 const navigation = useNavigation();
Acceptance checks in sandboxes, line comments and review verdicts239 const running = pulls.some(
240 (pull) =>
241 pull.status === "draft" ||
242 pull.checkStatus === "queued" ||
243 pull.checkStatus === "running",
244 );
Agents as a team: lifecycle, merge queue, billing and a new shell245 // The pull request g1t's agent has in progress for this issue, if any.
246 const assigned = [...pulls]
247 .reverse()
248 .find(
249 (pull) =>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent250 pull.agent === "g1t" &&
Agents as a team: lifecycle, merge queue, billing and a new shell251 pull.runtime === "hosted" &&
252 (pull.status === "draft" || pull.status === "open"),
253 );
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending254 useRefreshWhile(Boolean(running || assigned));
Issues and pull requests replace intents and attempts255
256 const starting = navigation.formData?.get("action") === "run-hosted";
257 const base = `/${params.owner}/${params.repo}`;
258 const open = issue.state === "open";
259 const reference = `${params.owner}/${params.repo}#${issue.number}`;
260 const resolver = pulls.find((pull) => pull.number === issue.resolvedBy);
261 // The merged pull request first, then the ones still in play, then the rest.
262 const rank = { merged: 0, open: 1, draft: 2, closed: 3 } as const;
263 const ordered = [...pulls].sort(
264 (a, b) => rank[a.status] - rank[b.status] || a.number - b.number,
265 );
266
267 return (
268 <div className="grid gap-8 lg:grid-cols-[1fr_19rem]">
269 <div className="min-w-0">
270 <h2 className="text-2xl font-semibold tracking-tight text-balance">
271 {issue.title} <span className="font-normal text-faint">#{issue.number}</span>
272 </h2>
273 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
274 <IssueState issue={issue} />
275 <span className="flex items-center gap-2">
276 <Avatar name={issue.author.username} size={18} />
277 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains278 <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 rights279 {issue.requestedBy && (
280 <>
281 for{" "}
282 <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "}
283 </>
284 )}
Issues and pull requests replace intents and attempts285 <TimeAgo at={issue.createdAt} />
286 </span>
287 </span>
288 {issue.labels.map((name) => (
289 <Label key={name} name={name} />
290 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell291 {open && issue.agent && <Assignee agent={issue.agent} />}
Issues and pull requests replace intents and attempts292 </div>
293
294 {issue.resolvedBy != null && (
295 <Link
296 to={`${base}/pull/${issue.resolvedBy}`}
297 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"
298 >
299 <GitMerge size={18} className="shrink-0 text-merged" />
300 <span>
301 Resolved by{" "}
302 <span className="font-medium">
303 {resolver?.title ?? "pull request"} #{issue.resolvedBy}
304 </span>
305 {resolver?.mergedBy && (
306 <span className="text-muted">
307 , merged by {resolver.mergedBy}{" "}
308 {resolver.mergedAt && <TimeAgo at={resolver.mergedAt} />}
309 </span>
310 )}
311 </span>
312 </Link>
313 )}
314
315 {issue.body && (
316 <div className="mt-5 rounded-xl border border-line bg-surface p-5">
Agents as a team: lifecycle, merge queue, billing and a new shell317 <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} />
Issues and pull requests replace intents and attempts318 </div>
319 )}
320
321 <div className="mt-10 flex items-baseline justify-between">
322 <h3 className="font-semibold tracking-tight">Pull requests</h3>
323 <p className="text-sm text-muted">
324 {pulls.length === 0
325 ? "None yet"
326 : `${pulls.length} for this issue${
327 pulls.some((pull) => pull.status === "merged") ? "" : ", none merged"
328 }`}
329 </p>
330 </div>
331 <div className="mt-3">
332 {pulls.length === 0 ? (
333 <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-agent334 Put g1t on it, or point your own agent at{" "}
Issues and pull requests replace intents and attempts335 <code className="font-mono">{reference}</code>.
336 </EmptyState>
337 ) : (
338 <ol className="space-y-3">
339 {ordered.map((pull) => (
340 <PullRow key={pull.id} pull={pull} base={base} />
341 ))}
342 </ol>
343 )}
344 </div>
345
346 <h3 className="mt-10 font-semibold tracking-tight">Discussion</h3>
347 <div className="mt-3">
Agents as a team: lifecycle, merge queue, billing and a new shell348 <div className="space-y-4">
349 <CommentList comments={comments} base={base} />
350 <CommentForm author={viewer?.username ?? null} resetKey={comments.length} />
351 {canManage && (
352 <Form method="post" className="flex flex-wrap justify-end gap-2">
353 {open ? (
354 <>
355 <Button variant="quiet" type="submit" name="action" value="close-not-planned">
356 Close as not planned
357 </Button>
358 <Button variant="quiet" type="submit" name="action" value="close-completed">
359 Close issue
360 </Button>
361 </>
362 ) : (
363 <Button variant="quiet" type="submit" name="action" value="reopen">
364 Reopen issue
365 </Button>
366 )}
367 </Form>
368 )}
369 </div>
Issues and pull requests replace intents and attempts370 </div>
371 <div className="mt-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look372 {!(actionData && "action" in actionData) && <ErrorText>{actionData?.error}</ErrorText>}
Issues and pull requests replace intents and attempts373 </div>
374 </div>
375
376 <aside className="space-y-6">
Integrations: your own model provider, alerts that open issues, tickets agents read377 {loaderData.links.length > 0 && (
378 <section>
379 <h3 className="text-sm font-medium">From outside g1t</h3>
380 <ul className="mt-2 space-y-1.5 text-sm">
381 {loaderData.links.map((link) => (
382 <li key={`${link.connectionId}-${link.key}`}>
383 <a
384 href={link.url}
385 target="_blank"
386 rel="noreferrer"
387 className="group flex items-center gap-2 rounded-lg border border-line px-2.5 py-2 transition-colors hover:border-line-strong"
388 >
389 <span className="min-w-0 grow">
390 <span className="block truncate font-medium">
391 {PROVIDERS[link.provider].label} <span className="font-mono text-muted">{link.key}</span>
392 </span>
393 <span className="block text-xs text-faint">
394 {link.count > 1 ? `Seen ${link.count} times, last ` : "Linked "}
395 <TimeAgo at={link.count > 1 ? link.lastSeen : link.firstSeen} />
396 </span>
397 </span>
398 <ExternalLink size={13} className="shrink-0 text-faint group-hover:text-fg" />
399 </a>
400 </li>
401 ))}
402 </ul>
403 </section>
404 )}
Agents as a team: lifecycle, merge queue, billing and a new shell405 <section>
406 <h3 className="text-sm font-medium">Assignees</h3>
407 <ul className="mt-2 space-y-1.5 text-sm">
408 {open && assigned && (
409 <li>
410 <Link
411 to={`${base}/pull/${assigned.number}`}
412 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"
413 >
414 <Sparkles size={15} className="shrink-0 text-accent" />
415 <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-agent416 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell417 <span className="block truncate text-xs text-muted">
418 {assigned.status === "draft"
419 ? "Making the change"
420 : "Seeing it through checks and review"}{" "}
421 · #{assigned.number}
422 </span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains423 <AgentStepLine owner={params.owner} repo={params.repo} number={assigned.number} />
Agents as a team: lifecycle, merge queue, billing and a new shell424 </span>
425 <span className="size-1.5 shrink-0 animate-pulse rounded-full bg-accent" />
426 </Link>
427 </li>
428 )}
429 {issue.assignees.map((name) => (
430 <li key={name} className="flex items-center gap-2 px-1">
431 <Avatar name={name} size={20} />
432 <span className="grow truncate font-mono text-xs">{name}</span>
433 </li>
434 ))}
435 {open && issue.queued && !assigned && (
436 <li className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2">
437 <Sparkles size={15} className="shrink-0 text-faint" />
438 <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-agent439 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell440 <span className="block text-xs text-muted">
441 {issue.blockedBy.length > 0
442 ? "Queued. Starts when what this depends on has merged."
443 : "Queued. Starts as soon as there is room."}
444 </span>
445 </span>
446 </li>
447 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent448 {!open && resolver && resolver.agent === "g1t" && (
Agents as a team: lifecycle, merge queue, billing and a new shell449 <li>
450 <Link
451 to={`${base}/pull/${resolver.number}`}
452 className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2 transition-colors hover:border-line-strong"
453 >
454 <Sparkles size={15} className="shrink-0 text-merged" />
455 <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-agent456 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell457 <span className="block truncate text-xs text-muted">
458 Resolved it with #{resolver.number}
459 </span>
460 </span>
461 </Link>
462 </li>
463 )}
464 {issue.assignees.length === 0 &&
465 !(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-agent466 !(!open && resolver?.agent === "g1t") && (
Agents as a team: lifecycle, merge queue, billing and a new shell467 <li className="px-1 text-xs text-faint">No one yet.</li>
468 )}
469 </ul>
470 {issue.blockedBy.length > 0 && (
471 <p className="mt-3 text-xs text-muted">
472 Depends on{" "}
473 {issue.blockedBy.map((number, index) => (
474 <span key={number}>
475 {index > 0 && ", "}
476 <Link
477 to={`${base}/issues/${number}`}
478 className="font-medium text-fg hover:underline"
479 >
480 #{number}
481 </Link>
482 </span>
483 ))}
484 {open ? ", which has to merge first." : "."}
Issues and pull requests replace intents and attempts485 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell486 )}
487
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look488 {open && agentsEnabled && can.run && !assigned && !issue.queued && (
Issues and pull requests replace intents and attempts489 <Form method="post" className="mt-3 space-y-2">
490 <input type="hidden" name="action" value="run-hosted" />
491 <div className="*:w-full">
492 <Button variant="accent" type="submit" disabled={starting}>
Agents as a team: lifecycle, merge queue, billing and a new shell493 <Sparkles size={14} />
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent494 {starting ? "Starting a sandbox…" : "Assign to g1t"}
Issues and pull requests replace intents and attempts495 </Button>
496 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell497 <details>
498 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
499 Add guidance for this run
500 </summary>
501 <div className="mt-2">
502 <Textarea
503 name="instructions"
504 rows={2}
505 placeholder="On top of the issue's description"
506 />
507 </div>
508 </details>
509 <p className="text-xs text-muted">
510 It opens a pull request and sees it through checks, a review by another
511 agent and fixes. You get it back ready to merge.
512 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look513 <ComputeNote note={loaderData.computeNote} />
514 {actionData && "action" in actionData && actionData.action === "run-hosted" && (
515 <ErrorText>{actionData.error}</ErrorText>
516 )}
Issues and pull requests replace intents and attempts517 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell518 )}
519
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look520 {open && !agentsEnabled && can.run && !assigned && !issue.queued && (
New workspaces are told to connect a model, before anything fails521 // Where g1t's agent would be, and what makes it appear.
522 <div className="mt-3 rounded-lg border border-dashed border-line p-3 text-sm">
523 <p className="flex items-center gap-1.5 font-medium">
524 <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-agent525 g1t
New workspaces are told to connect a model, before anything fails526 </p>
527 <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-agent528 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 look529 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 fails530 </p>
531 <Link
532 to={`/${params.owner}/-/integrations`}
533 className="mt-2 inline-block text-xs text-accent hover:underline"
534 >
535 Connect a model
536 </Link>
537 </div>
538 )}
539
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look540 {viewer && can.triage && open && (
Agents as a team: lifecycle, merge queue, billing and a new shell541 <div className="mt-3 space-y-2">
542 {!issue.assignees.includes(viewer.username) && (
543 <Form method="post">
544 <input type="hidden" name="action" value="assign" />
545 {issue.assignees.map((name) => (
546 <input key={name} type="hidden" name="assignee" value={name} />
547 ))}
548 <input type="hidden" name="assignee" value={viewer.username} />
549 <div className="*:w-full">
550 <Button variant="quiet" type="submit">
551 Assign yourself
552 </Button>
553 </div>
554 </Form>
555 )}
556 <details className="group">
557 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
558 Assign people
559 </summary>
560 <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}>
561 <input type="hidden" name="action" value="assign" />
562 <PeoplePicker name="assignee" members={members} chosen={issue.assignees} />
563 <Button variant="quiet" type="submit">
564 Save assignees
565 </Button>
566 </Form>
567 </details>
568 </div>
569 )}
570 </section>
Issues and pull requests replace intents and attempts571
572 {open && (
573 <section className="rounded-xl border border-line bg-surface p-4">
574 <h3 className="text-sm font-medium">Bring your own agent</h3>
575 <p className="mt-1 text-xs text-muted">
576 With g1t connected to your agent, ask it to work on this issue.
577 </p>
578 <div className="mt-3">
579 <CopyLine text={reference} />
580 </div>
581 {viewer ? (
582 <Form method="post" className="mt-4 space-y-2 border-t border-line pt-4">
583 <input type="hidden" name="action" value="open-pull" />
584 <p className="text-xs text-muted">
585 Or open a draft pull request yourself and get a fork to push to.
586 </p>
587 <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} />
588 <div className="*:w-full">
589 <Button type="submit">Open pull request</Button>
590 </div>
Pull requests from branches591 <p className="text-xs text-muted">
592 Already pushed a branch?{" "}
593 <Link
594 to={`${base}/pulls/new?issue=${issue.number}`}
595 className="text-fg underline underline-offset-4"
596 >
597 Open a pull request from it
598 </Link>
599 .
600 </p>
Issues and pull requests replace intents and attempts601 </Form>
602 ) : (
603 <p className="mt-4 border-t border-line pt-4 text-sm text-muted">
604 <Link to="/login" className="text-fg underline underline-offset-4">
605 Sign in
606 </Link>{" "}
607 to open a pull request.
608 </p>
609 )}
610 </section>
611 )}
612
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look613 {can.triage && (
Issues and pull requests replace intents and attempts614 <details className="group">
615 <summary className="cursor-pointer list-none text-sm font-medium">
616 Labels <span className="text-xs font-normal text-faint group-open:hidden">Edit</span>
617 </summary>
618 <Form method="post" className="mt-3 space-y-3" key={issue.labels.join()}>
619 <input type="hidden" name="action" value="labels" />
620 <div className="flex flex-wrap gap-x-3 gap-y-2">
621 {labels.map((name) => (
Workspace names and icons, and a component kit for every control622 <CheckboxOption
623 key={name}
624 name="label"
625 value={name}
626 defaultChecked={issue.labels.includes(name)}
627 label={<Label name={name} />}
628 className="items-center gap-1.5"
629 />
Issues and pull requests replace intents and attempts630 ))}
631 </div>
632 <Input name="labels" placeholder="New labels, comma separated" />
633 <Button variant="quiet" type="submit">
634 Save labels
635 </Button>
636 </Form>
637 </details>
638 )}
639
640 </aside>
641 </div>
642 );
643}