Skip to content

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

658 lines28,386 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";
Merge branch 'worktree-agent-a985c01f43538af01'3import { Form, Link, redirect } 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 CopyLine,
14 EmptyState,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look15 ComputeNote,
Issues and pull requests replace intents and attempts16 ErrorText,
17 Input,
Merge branch 'worktree-agent-a985c01f43538af01'18 SubmitButton,
Issues and pull requests replace intents and attempts19 Textarea,
20 TimeAgo,
21} from "../../components/ui";
Acceptance checks in sandboxes, line comments and review verdicts22import { CheckBadge } from "../../components/checks";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar23import { LabelChip, LabelsBox, MilestoneBox } from "../../components/labels";
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,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains32 PersonLink,
Agents as a team: lifecycle, merge queue, billing and a new shell33 PullIcon,
34 plainText,
35} from "../../components/work";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put36import { 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-agent37import { openedBy } from "../../lib/opened-by";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38import { computeNoteFor } from "../../lib/compute.server";
Merge branch 'worktree-agent-aca4790ea223aa235'39import { agents, identity, inbox, integrations, work } from "../../lib/services.server";
Agents as a team: lifecycle, merge queue, billing and a new shell40import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server";
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings41import { accessTo, refusal, repoFor } from "../../lib/access.server";
42import { SubscriptionBox } from "../../components/notifications";
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);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar69 const [{ can }, found, labels, milestones, agentsEnabled, members, links, computeNote, subscription, active, teams] = await Promise.all([
Fast pages, required checks on the branch, self-hosted runners, honest incidents70 access,
Agents as a team: lifecycle, merge queue, billing and a new shell71 work.getIssue(path, number, viewer),
72 work.listLabels(path, viewer),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar73 // Open ones to choose from, and the one it is in, with its progress.
74 work.listMilestones(path, viewer),
g1t's agents only for listed workspaces, whatever the state of billing75 env.RUNNER.enabled(viewer, path),
Agents as a team: lifecycle, merge queue, billing and a new shell76 // A member picks assignees from the workspace's people.
77 roleIn(viewer, params.owner) ? identity.listMembers(params.owner, viewer) : null,
Integrations: your own model provider, alerts that open issues, tickets agents read78 // What it is tied to outside g1t. Shown only once the issue is known visible.
79 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-agent80 // Before a member assigns g1t: whether the workspace's plan lets it start.
Fast pages, required checks on the branch, self-hosted runners, honest incidents81 access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)),
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings82 // Whether the viewer hears of it, for the sidebar's Notifications.
83 viewer
84 ? repoFor(context, params).then((repo) =>
85 repo.ok ? inbox.subscription(viewer, { repoId: repo.value.id, number }).catch(() => null) : null,
86 )
87 : null,
Merge branch 'worktree-agent-aca4790ea223aa235'88 // The project's agents at work, for the line under the pull request
89 // one is on for this issue: with the page, not after it.
90 agents.listRuns(viewer, { repo: path, active: true, limit: 50 }).catch(() => null),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar91 // The teams a member can mention, for the comment box.
92 roleIn(viewer, params.owner) ? identity.listTeams(viewer, params.owner).catch(() => null) : null,
Agents as a team: lifecycle, merge queue, billing and a new shell93 ]);
Issues and pull requests replace intents and attempts94 if (!found.ok) {
95 // Issues and pull requests share numbers; this one may be a pull request.
96 const pull = await work.getPull(path, number, viewer);
97 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 put98 throw notFound("issue");
Issues and pull requests replace intents and attempts99 }
100 const { issue } = found.value;
101 return {
102 ...found.value,
103 viewer,
104 labels: labels.ok ? labels.value : [],
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar105 milestones: milestones.ok ? milestones.value : [],
Agents as a team: lifecycle, merge queue, billing and a new shell106 agentsEnabled,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look107 computeNote,
Integrations: your own model provider, alerts that open issues, tickets agents read108 links,
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings109 subscription,
Merge branch 'worktree-agent-aca4790ea223aa235'110 // As the project's agents.json has them; left out, the line fetches them.
111 activeRuns: active?.ok ? { runs: active.value, member: can.run } : undefined,
Agents as a team: lifecycle, merge queue, billing and a new shell112 members: members?.ok ? members.value.map((member) => member.username) : [],
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar113 handles: teams?.ok ? teams.value.map((team) => `@${team.workspace}/${team.slug}`) : [],
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights114 // The author can close and reopen their own issue, and whoever g1t's
115 // agent filed one for, that one; Triage and up, anyone's.
116 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 look117 can,
Issues and pull requests replace intents and attempts118 };
119}
120
121export async function action({ request, params, context }: Route.ActionArgs) {
122 assertSameOrigin(request);
123 const user = requireUser(context, request);
124 const form = await request.formData();
125 const path = { namespace: params.owner, name: params.repo };
126 const number = Number(params.number);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look127 // What each form needs; closing your own issue is checked by work.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar128 // Labels: its author may use the repository's on their own issue, and
129 // work says so; anyone else, and every milestone, needs Triage.
130 const needed =
131 form.get("action") === "run-hosted"
132 ? "run"
133 : form.get("action") === "assign" || form.get("action") === "milestone"
134 ? "triage"
135 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look136 const refused = needed ? await refusal(context, params, needed) : null;
137 if (refused) return { error: refused, action: String(form.get("action")) };
Issues and pull requests replace intents and attempts138
139 switch (form.get("action")) {
140 case "run-hosted": {
141 const result = await env.RUNNER.run(user, path, number, {
142 instructions: String(form.get("instructions") ?? ""),
143 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look144 return result.ok ? null : { error: result.error.message, action: "run-hosted" };
Issues and pull requests replace intents and attempts145 }
146 case "open-pull": {
147 const result = await work.openPull(user, path, {
148 issue: number,
149 agent: String(form.get("agent") ?? ""),
150 runtime: "external",
151 });
152 if (!result.ok) return { error: result.error.message };
153 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
154 }
155 case "comment": {
Acceptance checks in sandboxes, line comments and review verdicts156 const result = await work.addComment(user, path, number, {
157 body: String(form.get("body") ?? ""),
158 });
Issues and pull requests replace intents and attempts159 return result.ok ? null : { error: result.error.message };
160 }
Agents as a team: lifecycle, merge queue, billing and a new shell161 case "assign": {
162 const result = await work.updateIssue(user, path, number, {
163 assignees: [
164 ...form.getAll("assignee").map(String),
165 ...String(form.get("others") ?? "").split(/[\s,]+/),
166 ],
167 });
168 return result.ok ? null : { error: result.error.message };
169 }
Issues and pull requests replace intents and attempts170 case "labels": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar171 const result = await work.setLabels(user, path, number, form.getAll("label").map(String));
172 return result.ok ? null : { error: result.error.message, action: "labels" };
Issues and pull requests replace intents and attempts173 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar174 case "milestone": {
175 const result = await work.updateIssue(user, path, number, { milestone: Number(form.get("milestone")) || 0 });
176 return result.ok ? null : { error: result.error.message, action: "milestone" };
177 }
Issues and pull requests replace intents and attempts178 case "reopen": {
179 const result = await work.reopenIssue(user, path, number);
180 return result.ok ? null : { error: result.error.message };
181 }
182 default: {
183 const result = await work.closeIssue(
184 user,
185 path,
186 number,
187 form.get("action") === "close-not-planned" ? "not_planned" : "completed",
188 );
189 return result.ok ? null : { error: result.error.message };
190 }
191 }
192}
193
194/** What became of a pull request, in a few words. */
195function outcome(pull: Pull): string {
196 if (pull.status === "merged") return `Merged by ${pull.mergedBy ?? "someone"}`;
197 if (pull.supersededBy != null) return `Closed · #${pull.supersededBy} was merged instead`;
198 if (pull.status === "closed") return "Closed without merging";
199 if (pull.status === "draft") return "Draft · in progress";
200 return "Ready for review";
201}
202
203function PullRow({ pull, base }: { pull: Pull; base: string }) {
204 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-agent205 const opener = openedBy(pull);
Issues and pull requests replace intents and attempts206 return (
207 <li>
208 <Link
209 to={`${base}/pull/${pull.number}`}
210 className={`block rounded-xl border p-4 transition-colors ${
211 merged
212 ? "border-merged/40 bg-merged/5 hover:border-merged/70"
213 : "border-line bg-surface hover:border-line-strong"
214 } ${pull.status === "closed" ? "opacity-70" : ""}`}
215 >
216 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
217 <PullIcon status={pull.status} />
218 <span className="font-medium">
219 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
220 </span>
221 {pull.status === "draft" && (
222 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
223 )}
224 <span className="ml-auto flex items-center gap-3 text-xs text-faint">
Agents as a team: lifecycle, merge queue, billing and a new shell225 <ChangeSize files={pull.files} />
Acceptance checks in sandboxes, line comments and review verdicts226 <CheckBadge status={pull.checkStatus} />
Issues and pull requests replace intents and attempts227 <span className="flex items-center gap-1 font-mono">
228 <GitCommitHorizontal size={13} />
229 {pull.headCommit?.slice(0, 7) ?? "no commits"}
230 </span>
231 <TimeAgo at={pull.updatedAt} />
232 </span>
233 </div>
234 <p className="mt-1.5 flex flex-wrap items-center gap-x-2 text-xs text-muted">
235 <span className={merged ? "font-medium text-merged" : ""}>{outcome(pull)}</span>
236 <span className="text-faint">·</span>
237 <span className="flex items-center gap-1 font-mono">
238 <Bot size={12} />
239 {pull.agent}
240 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent241 {opener.requestedBy ? (
242 <span className="text-faint">· requested by {opener.requestedBy}</span>
243 ) : (
244 <>
245 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
246 <span className="text-faint">· opened by {opener.name}</span>
247 </>
248 )}
Issues and pull requests replace intents and attempts249 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell250 {pull.body && (
251 <p className="mt-2 line-clamp-2 text-sm text-muted">{plainText(pull.body)}</p>
252 )}
Issues and pull requests replace intents and attempts253 </Link>
254 </li>
255 );
256}
257
258export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look259 const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData;
Issues and pull requests replace intents and attempts260
261 // Follow agents at work without a manual reload.
Acceptance checks in sandboxes, line comments and review verdicts262 const running = pulls.some(
263 (pull) =>
264 pull.status === "draft" ||
265 pull.checkStatus === "queued" ||
266 pull.checkStatus === "running",
267 );
Agents as a team: lifecycle, merge queue, billing and a new shell268 // The pull request g1t's agent has in progress for this issue, if any.
269 const assigned = [...pulls]
270 .reverse()
271 .find(
272 (pull) =>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent273 pull.agent === "g1t" &&
Agents as a team: lifecycle, merge queue, billing and a new shell274 pull.runtime === "hosted" &&
275 (pull.status === "draft" || pull.status === "open"),
276 );
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending277 useRefreshWhile(Boolean(running || assigned));
Issues and pull requests replace intents and attempts278
Merge branch 'worktree-agent-a985c01f43538af01'279 // A refusal to start g1t shows by its button; every other error below the discussion.
280 const runError = actionData && "action" in actionData && actionData.action === "run-hosted";
Issues and pull requests replace intents and attempts281 const base = `/${params.owner}/${params.repo}`;
282 const open = issue.state === "open";
283 const reference = `${params.owner}/${params.repo}#${issue.number}`;
284 const resolver = pulls.find((pull) => pull.number === issue.resolvedBy);
285 // The merged pull request first, then the ones still in play, then the rest.
286 const rank = { merged: 0, open: 1, draft: 2, closed: 3 } as const;
287 const ordered = [...pulls].sort(
288 (a, b) => rank[a.status] - rank[b.status] || a.number - b.number,
289 );
290
291 return (
292 <div className="grid gap-8 lg:grid-cols-[1fr_19rem]">
293 <div className="min-w-0">
294 <h2 className="text-2xl font-semibold tracking-tight text-balance">
295 {issue.title} <span className="font-normal text-faint">#{issue.number}</span>
296 </h2>
297 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
298 <IssueState issue={issue} />
299 <span className="flex items-center gap-2">
300 <Avatar name={issue.author.username} size={18} />
301 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains302 <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 rights303 {issue.requestedBy && (
304 <>
305 for{" "}
306 <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "}
307 </>
308 )}
Issues and pull requests replace intents and attempts309 <TimeAgo at={issue.createdAt} />
310 </span>
311 </span>
312 {issue.labels.map((name) => (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar313 <LabelChip key={name} name={name} color={labels.find((label) => label.name === name)?.color} />
Issues and pull requests replace intents and attempts314 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell315 {open && issue.agent && <Assignee agent={issue.agent} />}
Issues and pull requests replace intents and attempts316 </div>
317
318 {issue.resolvedBy != null && (
319 <Link
320 to={`${base}/pull/${issue.resolvedBy}`}
321 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"
322 >
323 <GitMerge size={18} className="shrink-0 text-merged" />
324 <span>
325 Resolved by{" "}
326 <span className="font-medium">
327 {resolver?.title ?? "pull request"} #{issue.resolvedBy}
328 </span>
329 {resolver?.mergedBy && (
330 <span className="text-muted">
331 , merged by {resolver.mergedBy}{" "}
332 {resolver.mergedAt && <TimeAgo at={resolver.mergedAt} />}
333 </span>
334 )}
335 </span>
336 </Link>
337 )}
338
339 {issue.body && (
340 <div className="mt-5 rounded-xl border border-line bg-surface p-5">
Agents as a team: lifecycle, merge queue, billing and a new shell341 <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} />
Issues and pull requests replace intents and attempts342 </div>
343 )}
344
345 <div className="mt-10 flex items-baseline justify-between">
346 <h3 className="font-semibold tracking-tight">Pull requests</h3>
347 <p className="text-sm text-muted">
348 {pulls.length === 0
349 ? "None yet"
350 : `${pulls.length} for this issue${
351 pulls.some((pull) => pull.status === "merged") ? "" : ", none merged"
352 }`}
353 </p>
354 </div>
355 <div className="mt-3">
356 {pulls.length === 0 ? (
357 <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-agent358 Put g1t on it, or point your own agent at{" "}
Issues and pull requests replace intents and attempts359 <code className="font-mono">{reference}</code>.
360 </EmptyState>
361 ) : (
362 <ol className="space-y-3">
363 {ordered.map((pull) => (
364 <PullRow key={pull.id} pull={pull} base={base} />
365 ))}
366 </ol>
367 )}
368 </div>
369
370 <h3 className="mt-10 font-semibold tracking-tight">Discussion</h3>
371 <div className="mt-3">
Agents as a team: lifecycle, merge queue, billing and a new shell372 <div className="space-y-4">
373 <CommentList comments={comments} base={base} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar374 <CommentForm author={viewer?.username ?? null} resetKey={comments.length} handles={loaderData.handles} />
Agents as a team: lifecycle, merge queue, billing and a new shell375 {canManage && (
376 <Form method="post" className="flex flex-wrap justify-end gap-2">
377 {open ? (
378 <>
Merge branch 'worktree-agent-a985c01f43538af01'379 <SubmitButton variant="quiet" name="action" value="close-not-planned" pending="Closing…">
Agents as a team: lifecycle, merge queue, billing and a new shell380 Close as not planned
Merge branch 'worktree-agent-a985c01f43538af01'381 </SubmitButton>
382 <SubmitButton variant="quiet" name="action" value="close-completed" pending="Closing…">
Agents as a team: lifecycle, merge queue, billing and a new shell383 Close issue
Merge branch 'worktree-agent-a985c01f43538af01'384 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell385 </>
386 ) : (
Merge branch 'worktree-agent-a985c01f43538af01'387 <SubmitButton variant="quiet" name="action" value="reopen" pending="Reopening…">
Agents as a team: lifecycle, merge queue, billing and a new shell388 Reopen issue
Merge branch 'worktree-agent-a985c01f43538af01'389 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell390 )}
391 </Form>
392 )}
393 </div>
Issues and pull requests replace intents and attempts394 </div>
395 <div className="mt-2">
Merge branch 'worktree-agent-a985c01f43538af01'396 {!runError && <ErrorText>{actionData?.error}</ErrorText>}
Issues and pull requests replace intents and attempts397 </div>
398 </div>
399
400 <aside className="space-y-6">
Integrations: your own model provider, alerts that open issues, tickets agents read401 {loaderData.links.length > 0 && (
402 <section>
403 <h3 className="text-sm font-medium">From outside g1t</h3>
404 <ul className="mt-2 space-y-1.5 text-sm">
405 {loaderData.links.map((link) => (
406 <li key={`${link.connectionId}-${link.key}`}>
407 <a
408 href={link.url}
409 target="_blank"
410 rel="noreferrer"
411 className="group flex items-center gap-2 rounded-lg border border-line px-2.5 py-2 transition-colors hover:border-line-strong"
412 >
413 <span className="min-w-0 grow">
414 <span className="block truncate font-medium">
415 {PROVIDERS[link.provider].label} <span className="font-mono text-muted">{link.key}</span>
416 </span>
417 <span className="block text-xs text-faint">
418 {link.count > 1 ? `Seen ${link.count} times, last ` : "Linked "}
419 <TimeAgo at={link.count > 1 ? link.lastSeen : link.firstSeen} />
420 </span>
421 </span>
422 <ExternalLink size={13} className="shrink-0 text-faint group-hover:text-fg" />
423 </a>
424 </li>
425 ))}
426 </ul>
427 </section>
428 )}
Agents as a team: lifecycle, merge queue, billing and a new shell429 <section>
430 <h3 className="text-sm font-medium">Assignees</h3>
431 <ul className="mt-2 space-y-1.5 text-sm">
432 {open && assigned && (
433 <li>
434 <Link
435 to={`${base}/pull/${assigned.number}`}
436 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"
437 >
438 <Sparkles size={15} className="shrink-0 text-accent" />
439 <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-agent440 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell441 <span className="block truncate text-xs text-muted">
442 {assigned.status === "draft"
443 ? "Making the change"
444 : "Seeing it through checks and review"}{" "}
445 · #{assigned.number}
446 </span>
Merge branch 'worktree-agent-aca4790ea223aa235'447 <AgentStepLine owner={params.owner} repo={params.repo} number={assigned.number} runs={loaderData.activeRuns} />
Agents as a team: lifecycle, merge queue, billing and a new shell448 </span>
449 <span className="size-1.5 shrink-0 animate-pulse rounded-full bg-accent" />
450 </Link>
451 </li>
452 )}
453 {issue.assignees.map((name) => (
454 <li key={name} className="flex items-center gap-2 px-1">
455 <Avatar name={name} size={20} />
456 <span className="grow truncate font-mono text-xs">{name}</span>
457 </li>
458 ))}
459 {open && issue.queued && !assigned && (
460 <li className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2">
461 <Sparkles size={15} className="shrink-0 text-faint" />
462 <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-agent463 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell464 <span className="block text-xs text-muted">
465 {issue.blockedBy.length > 0
466 ? "Queued. Starts when what this depends on has merged."
467 : "Queued. Starts as soon as there is room."}
468 </span>
469 </span>
470 </li>
471 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent472 {!open && resolver && resolver.agent === "g1t" && (
Agents as a team: lifecycle, merge queue, billing and a new shell473 <li>
474 <Link
475 to={`${base}/pull/${resolver.number}`}
476 className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2 transition-colors hover:border-line-strong"
477 >
Merge branch 'worktree-agent-ad7c6d88d93adc817'478 <Sparkles size={15} className="shrink-0 text-accent" />
Agents as a team: lifecycle, merge queue, billing and a new shell479 <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-agent480 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell481 <span className="block truncate text-xs text-muted">
482 Resolved it with #{resolver.number}
483 </span>
484 </span>
485 </Link>
486 </li>
487 )}
488 {issue.assignees.length === 0 &&
489 !(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-agent490 !(!open && resolver?.agent === "g1t") && (
Agents as a team: lifecycle, merge queue, billing and a new shell491 <li className="px-1 text-xs text-faint">No one yet.</li>
492 )}
493 </ul>
494 {issue.blockedBy.length > 0 && (
495 <p className="mt-3 text-xs text-muted">
496 Depends on{" "}
497 {issue.blockedBy.map((number, index) => (
498 <span key={number}>
499 {index > 0 && ", "}
500 <Link
501 to={`${base}/issues/${number}`}
502 className="font-medium text-fg hover:underline"
503 >
504 #{number}
505 </Link>
506 </span>
507 ))}
508 {open ? ", which has to merge first." : "."}
Issues and pull requests replace intents and attempts509 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell510 )}
511
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look512 {open && agentsEnabled && can.run && !assigned && !issue.queued && (
Issues and pull requests replace intents and attempts513 <Form method="post" className="mt-3 space-y-2">
514 <input type="hidden" name="action" value="run-hosted" />
515 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'516 <SubmitButton variant="accent" match={{ action: "run-hosted" }} pending="Starting a sandbox…">
Agents as a team: lifecycle, merge queue, billing and a new shell517 <Sparkles size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'518 Assign to g1t
519 </SubmitButton>
Issues and pull requests replace intents and attempts520 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell521 <details>
522 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
523 Add guidance for this run
524 </summary>
525 <div className="mt-2">
526 <Textarea
527 name="instructions"
528 rows={2}
529 placeholder="On top of the issue's description"
530 />
531 </div>
532 </details>
533 <p className="text-xs text-muted">
534 It opens a pull request and sees it through checks, a review by another
535 agent and fixes. You get it back ready to merge.
536 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look537 <ComputeNote note={loaderData.computeNote} />
Merge branch 'worktree-agent-a985c01f43538af01'538 {runError && <ErrorText>{actionData.error}</ErrorText>}
Issues and pull requests replace intents and attempts539 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell540 )}
541
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look542 {open && !agentsEnabled && can.run && !assigned && !issue.queued && (
New workspaces are told to connect a model, before anything fails543 // Where g1t's agent would be, and what makes it appear.
544 <div className="mt-3 rounded-lg border border-dashed border-line p-3 text-sm">
545 <p className="flex items-center gap-1.5 font-medium">
546 <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-agent547 g1t
New workspaces are told to connect a model, before anything fails548 </p>
549 <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-agent550 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 look551 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 fails552 </p>
553 <Link
554 to={`/${params.owner}/-/integrations`}
555 className="mt-2 inline-block text-xs text-accent hover:underline"
556 >
557 Connect a model
558 </Link>
559 </div>
560 )}
561
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 {viewer && can.triage && open && (
Agents as a team: lifecycle, merge queue, billing and a new shell563 <div className="mt-3 space-y-2">
564 {!issue.assignees.includes(viewer.username) && (
565 <Form method="post">
566 <input type="hidden" name="action" value="assign" />
567 {issue.assignees.map((name) => (
568 <input key={name} type="hidden" name="assignee" value={name} />
569 ))}
570 <input type="hidden" name="assignee" value={viewer.username} />
571 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'572 <SubmitButton variant="quiet" name="who" value="self" pending="Assigning…">
Agents as a team: lifecycle, merge queue, billing and a new shell573 Assign yourself
Merge branch 'worktree-agent-a985c01f43538af01'574 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell575 </div>
576 </Form>
577 )}
578 <details className="group">
579 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
580 Assign people
581 </summary>
582 <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}>
583 <input type="hidden" name="action" value="assign" />
584 <PeoplePicker name="assignee" members={members} chosen={issue.assignees} />
Merge branch 'worktree-agent-a985c01f43538af01'585 <SubmitButton variant="quiet" name="who" value="picked" pending="Saving…">
Agents as a team: lifecycle, merge queue, billing and a new shell586 Save assignees
Merge branch 'worktree-agent-a985c01f43538af01'587 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell588 </Form>
589 </details>
590 </div>
591 )}
592 </section>
Issues and pull requests replace intents and attempts593
An issue being worked on no longer offers Bring your own agent beside the pull request doing it594 {/* Another way to start the work: not once a pull request is doing it. */}
595 {open && !pulls.some((pull) => pull.status === "open" || pull.status === "draft") && (
Issues and pull requests replace intents and attempts596 <section className="rounded-xl border border-line bg-surface p-4">
597 <h3 className="text-sm font-medium">Bring your own agent</h3>
598 <p className="mt-1 text-xs text-muted">
599 With g1t connected to your agent, ask it to work on this issue.
600 </p>
601 <div className="mt-3">
602 <CopyLine text={reference} />
603 </div>
604 {viewer ? (
605 <Form method="post" className="mt-4 space-y-2 border-t border-line pt-4">
606 <input type="hidden" name="action" value="open-pull" />
607 <p className="text-xs text-muted">
608 Or open a draft pull request yourself and get a fork to push to.
609 </p>
610 <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} />
611 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'612 <SubmitButton match={{ action: "open-pull" }} pending="Opening…">
613 Open pull request
614 </SubmitButton>
Issues and pull requests replace intents and attempts615 </div>
Pull requests from branches616 <p className="text-xs text-muted">
617 Already pushed a branch?{" "}
618 <Link
619 to={`${base}/pulls/new?issue=${issue.number}`}
620 className="text-fg underline underline-offset-4"
621 >
622 Open a pull request from it
623 </Link>
624 .
625 </p>
Issues and pull requests replace intents and attempts626 </Form>
627 ) : (
628 <p className="mt-4 border-t border-line pt-4 text-sm text-muted">
629 <Link to="/login" className="text-fg underline underline-offset-4">
630 Sign in
631 </Link>{" "}
632 to open a pull request.
633 </p>
634 )}
635 </section>
636 )}
637
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar638 <LabelsBox
639 labels={labels}
640 chosen={issue.labels}
641 canEdit={loaderData.canManage}
642 canCreate={can.triage}
643 manageUrl={can.triage ? `${base}/labels` : undefined}
644 />
645 <MilestoneBox
646 milestones={loaderData.milestones}
647 current={issue.milestone}
648 canEdit={can.triage}
649 base={base}
650 />
Issues and pull requests replace intents and attempts651
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings652 {viewer && (
653 <SubscriptionBox action={`${base}/notifications`} number={issue.number} kind="issue" subscription={loaderData.subscription} />
654 )}
Issues and pull requests replace intents and attempts655 </aside>
656 </div>
657 );
658}

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