Skip to content

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

679 lines29,455 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),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts117 // Maintain and up edit and delete anyone's comments; everyone, their own.
118 canModerate: can.manage_settings,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 can,
Issues and pull requests replace intents and attempts120 };
121}
122
123export async function action({ request, params, context }: Route.ActionArgs) {
124 assertSameOrigin(request);
125 const user = requireUser(context, request);
126 const form = await request.formData();
127 const path = { namespace: params.owner, name: params.repo };
128 const number = Number(params.number);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 // 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 bar130 // Labels: its author may use the repository's on their own issue, and
131 // work says so; anyone else, and every milestone, needs Triage.
132 const needed =
133 form.get("action") === "run-hosted"
134 ? "run"
135 : form.get("action") === "assign" || form.get("action") === "milestone"
136 ? "triage"
137 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look138 const refused = needed ? await refusal(context, params, needed) : null;
139 if (refused) return { error: refused, action: String(form.get("action")) };
Issues and pull requests replace intents and attempts140
141 switch (form.get("action")) {
142 case "run-hosted": {
143 const result = await env.RUNNER.run(user, path, number, {
144 instructions: String(form.get("instructions") ?? ""),
145 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look146 return result.ok ? null : { error: result.error.message, action: "run-hosted" };
Issues and pull requests replace intents and attempts147 }
148 case "open-pull": {
149 const result = await work.openPull(user, path, {
150 issue: number,
151 agent: String(form.get("agent") ?? ""),
152 runtime: "external",
153 });
154 if (!result.ok) return { error: result.error.message };
155 throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`);
156 }
157 case "comment": {
Acceptance checks in sandboxes, line comments and review verdicts158 const result = await work.addComment(user, path, number, {
159 body: String(form.get("body") ?? ""),
160 });
Issues and pull requests replace intents and attempts161 return result.ok ? null : { error: result.error.message };
162 }
Agents as a team: lifecycle, merge queue, billing and a new shell163 case "assign": {
164 const result = await work.updateIssue(user, path, number, {
165 assignees: [
166 ...form.getAll("assignee").map(String),
167 ...String(form.get("others") ?? "").split(/[\s,]+/),
168 ],
169 });
170 return result.ok ? null : { error: result.error.message };
171 }
Issues and pull requests replace intents and attempts172 case "labels": {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar173 const result = await work.setLabels(user, path, number, form.getAll("label").map(String));
174 return result.ok ? null : { error: result.error.message, action: "labels" };
Issues and pull requests replace intents and attempts175 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar176 case "milestone": {
177 const result = await work.updateIssue(user, path, number, { milestone: Number(form.get("milestone")) || 0 });
178 return result.ok ? null : { error: result.error.message, action: "milestone" };
179 }
Issues and pull requests replace intents and attempts180 case "reopen": {
181 const result = await work.reopenIssue(user, path, number);
182 return result.ok ? null : { error: result.error.message };
183 }
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts184 // A comment's own text, changed or taken away: its author's to do, or
185 // a maintainer's, which work checks.
186 case "edit-comment":
187 case "delete-comment": {
188 const action = String(form.get("action"));
189 const id = String(form.get("comment") ?? "");
190 const result =
191 action === "edit-comment"
192 ? await work.editComment(user, path, id, String(form.get("body") ?? ""))
193 : await work.deleteComment(user, path, id);
194 return result.ok ? null : { error: result.error.message, action, comment: id };
195 }
Issues and pull requests replace intents and attempts196 default: {
197 const result = await work.closeIssue(
198 user,
199 path,
200 number,
201 form.get("action") === "close-not-planned" ? "not_planned" : "completed",
202 );
203 return result.ok ? null : { error: result.error.message };
204 }
205 }
206}
207
208/** What became of a pull request, in a few words. */
209function outcome(pull: Pull): string {
210 if (pull.status === "merged") return `Merged by ${pull.mergedBy ?? "someone"}`;
211 if (pull.supersededBy != null) return `Closed · #${pull.supersededBy} was merged instead`;
212 if (pull.status === "closed") return "Closed without merging";
213 if (pull.status === "draft") return "Draft · in progress";
214 return "Ready for review";
215}
216
217function PullRow({ pull, base }: { pull: Pull; base: string }) {
218 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-agent219 const opener = openedBy(pull);
Issues and pull requests replace intents and attempts220 return (
221 <li>
222 <Link
223 to={`${base}/pull/${pull.number}`}
224 className={`block rounded-xl border p-4 transition-colors ${
225 merged
226 ? "border-merged/40 bg-merged/5 hover:border-merged/70"
227 : "border-line bg-surface hover:border-line-strong"
228 } ${pull.status === "closed" ? "opacity-70" : ""}`}
229 >
230 <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
231 <PullIcon status={pull.status} />
232 <span className="font-medium">
233 {pull.title} <span className="font-normal text-faint">#{pull.number}</span>
234 </span>
235 {pull.status === "draft" && (
236 <span className="size-1.5 animate-pulse rounded-full bg-accent" />
237 )}
238 <span className="ml-auto flex items-center gap-3 text-xs text-faint">
Agents as a team: lifecycle, merge queue, billing and a new shell239 <ChangeSize files={pull.files} />
Acceptance checks in sandboxes, line comments and review verdicts240 <CheckBadge status={pull.checkStatus} />
Issues and pull requests replace intents and attempts241 <span className="flex items-center gap-1 font-mono">
242 <GitCommitHorizontal size={13} />
243 {pull.headCommit?.slice(0, 7) ?? "no commits"}
244 </span>
245 <TimeAgo at={pull.updatedAt} />
246 </span>
247 </div>
248 <p className="mt-1.5 flex flex-wrap items-center gap-x-2 text-xs text-muted">
249 <span className={merged ? "font-medium text-merged" : ""}>{outcome(pull)}</span>
250 <span className="text-faint">·</span>
251 <span className="flex items-center gap-1 font-mono">
252 <Bot size={12} />
253 {pull.agent}
254 </span>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent255 {opener.requestedBy ? (
256 <span className="text-faint">· requested by {opener.requestedBy}</span>
257 ) : (
258 <>
259 {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>}
260 <span className="text-faint">· opened by {opener.name}</span>
261 </>
262 )}
Issues and pull requests replace intents and attempts263 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell264 {pull.body && (
265 <p className="mt-2 line-clamp-2 text-sm text-muted">{plainText(pull.body)}</p>
266 )}
Issues and pull requests replace intents and attempts267 </Link>
268 </li>
269 );
270}
271
272export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look273 const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData;
Issues and pull requests replace intents and attempts274
275 // Follow agents at work without a manual reload.
Acceptance checks in sandboxes, line comments and review verdicts276 const running = pulls.some(
277 (pull) =>
278 pull.status === "draft" ||
279 pull.checkStatus === "queued" ||
280 pull.checkStatus === "running",
281 );
Agents as a team: lifecycle, merge queue, billing and a new shell282 // The pull request g1t's agent has in progress for this issue, if any.
283 const assigned = [...pulls]
284 .reverse()
285 .find(
286 (pull) =>
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent287 pull.agent === "g1t" &&
Agents as a team: lifecycle, merge queue, billing and a new shell288 pull.runtime === "hosted" &&
289 (pull.status === "draft" || pull.status === "open"),
290 );
A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending291 useRefreshWhile(Boolean(running || assigned));
Issues and pull requests replace intents and attempts292
Merge branch 'worktree-agent-a985c01f43538af01'293 // A refusal to start g1t shows by its button; every other error below the discussion.
294 const runError = actionData && "action" in actionData && actionData.action === "run-hosted";
Issues and pull requests replace intents and attempts295 const base = `/${params.owner}/${params.repo}`;
296 const open = issue.state === "open";
297 const reference = `${params.owner}/${params.repo}#${issue.number}`;
298 const resolver = pulls.find((pull) => pull.number === issue.resolvedBy);
299 // The merged pull request first, then the ones still in play, then the rest.
300 const rank = { merged: 0, open: 1, draft: 2, closed: 3 } as const;
301 const ordered = [...pulls].sort(
302 (a, b) => rank[a.status] - rank[b.status] || a.number - b.number,
303 );
304
305 return (
306 <div className="grid gap-8 lg:grid-cols-[1fr_19rem]">
307 <div className="min-w-0">
308 <h2 className="text-2xl font-semibold tracking-tight text-balance">
309 {issue.title} <span className="font-normal text-faint">#{issue.number}</span>
310 </h2>
311 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted">
312 <IssueState issue={issue} />
313 <span className="flex items-center gap-2">
314 <Avatar name={issue.author.username} size={18} />
315 <span>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains316 <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 rights317 {issue.requestedBy && (
318 <>
319 for{" "}
320 <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "}
321 </>
322 )}
Issues and pull requests replace intents and attempts323 <TimeAgo at={issue.createdAt} />
324 </span>
325 </span>
326 {issue.labels.map((name) => (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar327 <LabelChip key={name} name={name} color={labels.find((label) => label.name === name)?.color} />
Issues and pull requests replace intents and attempts328 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell329 {open && issue.agent && <Assignee agent={issue.agent} />}
Issues and pull requests replace intents and attempts330 </div>
331
332 {issue.resolvedBy != null && (
333 <Link
334 to={`${base}/pull/${issue.resolvedBy}`}
335 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"
336 >
337 <GitMerge size={18} className="shrink-0 text-merged" />
338 <span>
339 Resolved by{" "}
340 <span className="font-medium">
341 {resolver?.title ?? "pull request"} #{issue.resolvedBy}
342 </span>
343 {resolver?.mergedBy && (
344 <span className="text-muted">
345 , merged by {resolver.mergedBy}{" "}
346 {resolver.mergedAt && <TimeAgo at={resolver.mergedAt} />}
347 </span>
348 )}
349 </span>
350 </Link>
351 )}
352
353 {issue.body && (
354 <div className="mt-5 rounded-xl border border-line bg-surface p-5">
Agents as a team: lifecycle, merge queue, billing and a new shell355 <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} />
Issues and pull requests replace intents and attempts356 </div>
357 )}
358
359 <div className="mt-10 flex items-baseline justify-between">
360 <h3 className="font-semibold tracking-tight">Pull requests</h3>
361 <p className="text-sm text-muted">
362 {pulls.length === 0
363 ? "None yet"
364 : `${pulls.length} for this issue${
365 pulls.some((pull) => pull.status === "merged") ? "" : ", none merged"
366 }`}
367 </p>
368 </div>
369 <div className="mt-3">
370 {pulls.length === 0 ? (
371 <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-agent372 Put g1t on it, or point your own agent at{" "}
Issues and pull requests replace intents and attempts373 <code className="font-mono">{reference}</code>.
374 </EmptyState>
375 ) : (
376 <ol className="space-y-3">
377 {ordered.map((pull) => (
378 <PullRow key={pull.id} pull={pull} base={base} />
379 ))}
380 </ol>
381 )}
382 </div>
383
384 <h3 className="mt-10 font-semibold tracking-tight">Discussion</h3>
385 <div className="mt-3">
Agents as a team: lifecycle, merge queue, billing and a new shell386 <div className="space-y-4">
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts387 <CommentList
388 comments={comments}
389 base={base}
390 viewerId={viewer?.id ?? null}
391 canModerate={loaderData.canModerate}
392 failed={actionData && "comment" in actionData ? { comment: String(actionData.comment), error: actionData.error } : null}
393 />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar394 <CommentForm author={viewer?.username ?? null} resetKey={comments.length} handles={loaderData.handles} />
Agents as a team: lifecycle, merge queue, billing and a new shell395 {canManage && (
396 <Form method="post" className="flex flex-wrap justify-end gap-2">
397 {open ? (
398 <>
Merge branch 'worktree-agent-a985c01f43538af01'399 <SubmitButton variant="quiet" name="action" value="close-not-planned" pending="Closing…">
Agents as a team: lifecycle, merge queue, billing and a new shell400 Close as not planned
Merge branch 'worktree-agent-a985c01f43538af01'401 </SubmitButton>
402 <SubmitButton variant="quiet" name="action" value="close-completed" pending="Closing…">
Agents as a team: lifecycle, merge queue, billing and a new shell403 Close issue
Merge branch 'worktree-agent-a985c01f43538af01'404 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell405 </>
406 ) : (
Merge branch 'worktree-agent-a985c01f43538af01'407 <SubmitButton variant="quiet" name="action" value="reopen" pending="Reopening…">
Agents as a team: lifecycle, merge queue, billing and a new shell408 Reopen issue
Merge branch 'worktree-agent-a985c01f43538af01'409 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell410 )}
411 </Form>
412 )}
413 </div>
Issues and pull requests replace intents and attempts414 </div>
415 <div className="mt-2">
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts416 {/* A refused comment edit says so under the comment. */}
417 {!runError && !(actionData && "comment" in actionData) && <ErrorText>{actionData?.error}</ErrorText>}
Issues and pull requests replace intents and attempts418 </div>
419 </div>
420
421 <aside className="space-y-6">
Integrations: your own model provider, alerts that open issues, tickets agents read422 {loaderData.links.length > 0 && (
423 <section>
424 <h3 className="text-sm font-medium">From outside g1t</h3>
425 <ul className="mt-2 space-y-1.5 text-sm">
426 {loaderData.links.map((link) => (
427 <li key={`${link.connectionId}-${link.key}`}>
428 <a
429 href={link.url}
430 target="_blank"
431 rel="noreferrer"
432 className="group flex items-center gap-2 rounded-lg border border-line px-2.5 py-2 transition-colors hover:border-line-strong"
433 >
434 <span className="min-w-0 grow">
435 <span className="block truncate font-medium">
436 {PROVIDERS[link.provider].label} <span className="font-mono text-muted">{link.key}</span>
437 </span>
438 <span className="block text-xs text-faint">
439 {link.count > 1 ? `Seen ${link.count} times, last ` : "Linked "}
440 <TimeAgo at={link.count > 1 ? link.lastSeen : link.firstSeen} />
441 </span>
442 </span>
443 <ExternalLink size={13} className="shrink-0 text-faint group-hover:text-fg" />
444 </a>
445 </li>
446 ))}
447 </ul>
448 </section>
449 )}
Agents as a team: lifecycle, merge queue, billing and a new shell450 <section>
451 <h3 className="text-sm font-medium">Assignees</h3>
452 <ul className="mt-2 space-y-1.5 text-sm">
453 {open && assigned && (
454 <li>
455 <Link
456 to={`${base}/pull/${assigned.number}`}
457 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"
458 >
459 <Sparkles size={15} className="shrink-0 text-accent" />
460 <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-agent461 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell462 <span className="block truncate text-xs text-muted">
463 {assigned.status === "draft"
464 ? "Making the change"
465 : "Seeing it through checks and review"}{" "}
466 · #{assigned.number}
467 </span>
Merge branch 'worktree-agent-aca4790ea223aa235'468 <AgentStepLine owner={params.owner} repo={params.repo} number={assigned.number} runs={loaderData.activeRuns} />
Agents as a team: lifecycle, merge queue, billing and a new shell469 </span>
470 <span className="size-1.5 shrink-0 animate-pulse rounded-full bg-accent" />
471 </Link>
472 </li>
473 )}
474 {issue.assignees.map((name) => (
475 <li key={name} className="flex items-center gap-2 px-1">
476 <Avatar name={name} size={20} />
477 <span className="grow truncate font-mono text-xs">{name}</span>
478 </li>
479 ))}
480 {open && issue.queued && !assigned && (
481 <li className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2">
482 <Sparkles size={15} className="shrink-0 text-faint" />
483 <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-agent484 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell485 <span className="block text-xs text-muted">
486 {issue.blockedBy.length > 0
487 ? "Queued. Starts when what this depends on has merged."
488 : "Queued. Starts as soon as there is room."}
489 </span>
490 </span>
491 </li>
492 )}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent493 {!open && resolver && resolver.agent === "g1t" && (
Agents as a team: lifecycle, merge queue, billing and a new shell494 <li>
495 <Link
496 to={`${base}/pull/${resolver.number}`}
497 className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2 transition-colors hover:border-line-strong"
498 >
Merge branch 'worktree-agent-ad7c6d88d93adc817'499 <Sparkles size={15} className="shrink-0 text-accent" />
Agents as a team: lifecycle, merge queue, billing and a new shell500 <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-agent501 <span className="block font-mono text-xs font-medium">g1t</span>
Agents as a team: lifecycle, merge queue, billing and a new shell502 <span className="block truncate text-xs text-muted">
503 Resolved it with #{resolver.number}
504 </span>
505 </span>
506 </Link>
507 </li>
508 )}
509 {issue.assignees.length === 0 &&
510 !(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-agent511 !(!open && resolver?.agent === "g1t") && (
Agents as a team: lifecycle, merge queue, billing and a new shell512 <li className="px-1 text-xs text-faint">No one yet.</li>
513 )}
514 </ul>
515 {issue.blockedBy.length > 0 && (
516 <p className="mt-3 text-xs text-muted">
517 Depends on{" "}
518 {issue.blockedBy.map((number, index) => (
519 <span key={number}>
520 {index > 0 && ", "}
521 <Link
522 to={`${base}/issues/${number}`}
523 className="font-medium text-fg hover:underline"
524 >
525 #{number}
526 </Link>
527 </span>
528 ))}
529 {open ? ", which has to merge first." : "."}
Issues and pull requests replace intents and attempts530 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell531 )}
532
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look533 {open && agentsEnabled && can.run && !assigned && !issue.queued && (
Issues and pull requests replace intents and attempts534 <Form method="post" className="mt-3 space-y-2">
535 <input type="hidden" name="action" value="run-hosted" />
536 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'537 <SubmitButton variant="accent" match={{ action: "run-hosted" }} pending="Starting a sandbox…">
Agents as a team: lifecycle, merge queue, billing and a new shell538 <Sparkles size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'539 Assign to g1t
540 </SubmitButton>
Issues and pull requests replace intents and attempts541 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell542 <details>
543 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
544 Add guidance for this run
545 </summary>
546 <div className="mt-2">
547 <Textarea
548 name="instructions"
549 rows={2}
550 placeholder="On top of the issue's description"
551 />
552 </div>
553 </details>
554 <p className="text-xs text-muted">
555 It opens a pull request and sees it through checks, a review by another
556 agent and fixes. You get it back ready to merge.
557 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look558 <ComputeNote note={loaderData.computeNote} />
Merge branch 'worktree-agent-a985c01f43538af01'559 {runError && <ErrorText>{actionData.error}</ErrorText>}
Issues and pull requests replace intents and attempts560 </Form>
Agents as a team: lifecycle, merge queue, billing and a new shell561 )}
562
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look563 {open && !agentsEnabled && can.run && !assigned && !issue.queued && (
New workspaces are told to connect a model, before anything fails564 // Where g1t's agent would be, and what makes it appear.
565 <div className="mt-3 rounded-lg border border-dashed border-line p-3 text-sm">
566 <p className="flex items-center gap-1.5 font-medium">
567 <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-agent568 g1t
New workspaces are told to connect a model, before anything fails569 </p>
570 <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-agent571 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 look572 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 fails573 </p>
574 <Link
575 to={`/${params.owner}/-/integrations`}
576 className="mt-2 inline-block text-xs text-accent hover:underline"
577 >
578 Connect a model
579 </Link>
580 </div>
581 )}
582
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583 {viewer && can.triage && open && (
Agents as a team: lifecycle, merge queue, billing and a new shell584 <div className="mt-3 space-y-2">
585 {!issue.assignees.includes(viewer.username) && (
586 <Form method="post">
587 <input type="hidden" name="action" value="assign" />
588 {issue.assignees.map((name) => (
589 <input key={name} type="hidden" name="assignee" value={name} />
590 ))}
591 <input type="hidden" name="assignee" value={viewer.username} />
592 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'593 <SubmitButton variant="quiet" name="who" value="self" pending="Assigning…">
Agents as a team: lifecycle, merge queue, billing and a new shell594 Assign yourself
Merge branch 'worktree-agent-a985c01f43538af01'595 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell596 </div>
597 </Form>
598 )}
599 <details className="group">
600 <summary className="cursor-pointer text-xs text-faint hover:text-fg">
601 Assign people
602 </summary>
603 <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}>
604 <input type="hidden" name="action" value="assign" />
605 <PeoplePicker name="assignee" members={members} chosen={issue.assignees} />
Merge branch 'worktree-agent-a985c01f43538af01'606 <SubmitButton variant="quiet" name="who" value="picked" pending="Saving…">
Agents as a team: lifecycle, merge queue, billing and a new shell607 Save assignees
Merge branch 'worktree-agent-a985c01f43538af01'608 </SubmitButton>
Agents as a team: lifecycle, merge queue, billing and a new shell609 </Form>
610 </details>
611 </div>
612 )}
613 </section>
Issues and pull requests replace intents and attempts614
An issue being worked on no longer offers Bring your own agent beside the pull request doing it615 {/* Another way to start the work: not once a pull request is doing it. */}
616 {open && !pulls.some((pull) => pull.status === "open" || pull.status === "draft") && (
Issues and pull requests replace intents and attempts617 <section className="rounded-xl border border-line bg-surface p-4">
618 <h3 className="text-sm font-medium">Bring your own agent</h3>
619 <p className="mt-1 text-xs text-muted">
620 With g1t connected to your agent, ask it to work on this issue.
621 </p>
622 <div className="mt-3">
623 <CopyLine text={reference} />
624 </div>
625 {viewer ? (
626 <Form method="post" className="mt-4 space-y-2 border-t border-line pt-4">
627 <input type="hidden" name="action" value="open-pull" />
628 <p className="text-xs text-muted">
629 Or open a draft pull request yourself and get a fork to push to.
630 </p>
631 <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} />
632 <div className="*:w-full">
Merge branch 'worktree-agent-a985c01f43538af01'633 <SubmitButton match={{ action: "open-pull" }} pending="Opening…">
634 Open pull request
635 </SubmitButton>
Issues and pull requests replace intents and attempts636 </div>
Pull requests from branches637 <p className="text-xs text-muted">
638 Already pushed a branch?{" "}
639 <Link
640 to={`${base}/pulls/new?issue=${issue.number}`}
641 className="text-fg underline underline-offset-4"
642 >
643 Open a pull request from it
644 </Link>
645 .
646 </p>
Issues and pull requests replace intents and attempts647 </Form>
648 ) : (
649 <p className="mt-4 border-t border-line pt-4 text-sm text-muted">
650 <Link to="/login" className="text-fg underline underline-offset-4">
651 Sign in
652 </Link>{" "}
653 to open a pull request.
654 </p>
655 )}
656 </section>
657 )}
658
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar659 <LabelsBox
660 labels={labels}
661 chosen={issue.labels}
662 canEdit={loaderData.canManage}
Merge main (membership, two-factor, GitHub repo roles) into tokens663 canCreate={can.manage_labels}
664 manageUrl={can.manage_labels ? `${base}/labels` : undefined}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar665 />
666 <MilestoneBox
667 milestones={loaderData.milestones}
668 current={issue.milestone}
669 canEdit={can.triage}
670 base={base}
671 />
Issues and pull requests replace intents and attempts672
Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings673 {viewer && (
674 <SubscriptionBox action={`${base}/notifications`} number={issue.number} kind="issue" subscription={loaderData.subscription} />
675 )}
Issues and pull requests replace intents and attempts676 </aside>
677 </div>
678 );
679}

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