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 attempts | 1 | import { env } from "cloudflare:workers"; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 2 | import { Bot, ExternalLink, GitCommitHorizontal, GitMerge, Play, Sparkles } from "lucide-react"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 3 | import { Form, Link, redirect } from "react-router"; |
| Issues and pull requests replace intents and attempts | 4 | |
| g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights | 5 | import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts"; |
| Issues and pull requests replace intents and attempts | 6 | |
| 7 | import type { Route } from "./+types/issue"; | |
| Social cards for every page: og.g1t.sh | 8 | import { excerpt, page } from "../../lib/meta"; |
| Issues and pull requests replace intents and attempts | 9 | import { Markdown } from "../../components/markdown"; |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 10 | import { AgentStepLine } from "../../components/agents"; |
| Issues and pull requests replace intents and attempts | 11 | import { |
| 12 | Avatar, | |
| 13 | CopyLine, | |
| 14 | EmptyState, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 15 | ComputeNote, |
| Issues and pull requests replace intents and attempts | 16 | ErrorText, |
| 17 | Input, | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 18 | SubmitButton, |
| Issues and pull requests replace intents and attempts | 19 | Textarea, |
| 20 | TimeAgo, | |
| 21 | } from "../../components/ui"; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 22 | import { CheckBadge } from "../../components/checks"; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 23 | import { LabelChip, LabelsBox, MilestoneBox } from "../../components/labels"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 24 | import { |
| 25 | Assignee, | |
| 26 | AssigneeStack, | |
| 27 | ChangeSize, | |
| 28 | CommentForm, | |
| 29 | CommentList, | |
| 30 | PeoplePicker, | |
| 31 | IssueState, | |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 32 | PersonLink, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 33 | 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 put | 36 | import { 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-agent | 37 | import { openedBy } from "../../lib/opened-by"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 38 | import { computeNoteFor } from "../../lib/compute.server"; |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 39 | import { agents, identity, inbox, integrations, work } from "../../lib/services.server"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 40 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 41 | import { accessTo, refusal, repoFor } from "../../lib/access.server"; |
| 42 | import { SubscriptionBox } from "../../components/notifications"; | |
| A click away from a live page arrives: pages stop refreshing themselves while a navigation is pending | 43 | import { useRefreshWhile } from "../../lib/refresh"; |
| Issues and pull requests replace intents and attempts | 44 | |
| 45 | ||
| Social cards for every page: og.g1t.sh | 46 | export 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 rights | 54 | ? `${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.sh | 55 | : 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 attempts | 60 | } |
| 61 | ||
| 62 | export 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 incidents | 66 | // 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 bar | 69 | 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 incidents | 70 | access, |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 71 | 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 bar | 73 | // 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 billing | 75 | env.RUNNER.enabled(viewer, path), |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 76 | // 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 read | 78 | // 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-agent | 80 | // Before a member assigns g1t: whether the workspace's plan lets it start. |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 81 | access.then(({ can }) => (can.run ? computeNoteFor(params.owner, "agent") : null)), |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 82 | // 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 bar | 91 | // 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 shell | 93 | ]); |
| Issues and pull requests replace intents and attempts | 94 | 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 put | 98 | throw notFound("issue"); |
| Issues and pull requests replace intents and attempts | 99 | } |
| 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 bar | 105 | milestones: milestones.ok ? milestones.value : [], |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 106 | agentsEnabled, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 107 | computeNote, |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 108 | links, |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 109 | 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 shell | 112 | members: members?.ok ? members.value.map((member) => member.username) : [], |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 113 | 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 rights | 114 | // 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 timeouts | 117 | // 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 look | 119 | can, |
| Issues and pull requests replace intents and attempts | 120 | }; |
| 121 | } | |
| 122 | ||
| 123 | export 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 look | 129 | // 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 bar | 130 | // 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 look | 138 | 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 attempts | 140 | |
| 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 look | 146 | return result.ok ? null : { error: result.error.message, action: "run-hosted" }; |
| Issues and pull requests replace intents and attempts | 147 | } |
| 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 verdicts | 158 | const result = await work.addComment(user, path, number, { |
| 159 | body: String(form.get("body") ?? ""), | |
| 160 | }); | |
| Issues and pull requests replace intents and attempts | 161 | return result.ok ? null : { error: result.error.message }; |
| 162 | } | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 163 | 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 attempts | 172 | case "labels": { |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 173 | 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 attempts | 175 | } |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 176 | 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 attempts | 180 | 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 timeouts | 184 | // 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 attempts | 196 | 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. */ | |
| 209 | function 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 | ||
| 217 | function 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-agent | 219 | const opener = openedBy(pull); |
| Issues and pull requests replace intents and attempts | 220 | 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 shell | 239 | <ChangeSize files={pull.files} /> |
| Acceptance checks in sandboxes, line comments and review verdicts | 240 | <CheckBadge status={pull.checkStatus} /> |
| Issues and pull requests replace intents and attempts | 241 | <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-agent | 255 | {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 attempts | 263 | </p> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 264 | {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 attempts | 267 | </Link> |
| 268 | </li> | |
| 269 | ); | |
| 270 | } | |
| 271 | ||
| 272 | export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 273 | const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage, can } = loaderData; |
| Issues and pull requests replace intents and attempts | 274 | |
| 275 | // Follow agents at work without a manual reload. | |
| Acceptance checks in sandboxes, line comments and review verdicts | 276 | 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 shell | 282 | // 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-agent | 287 | pull.agent === "g1t" && |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 288 | 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 pending | 291 | useRefreshWhile(Boolean(running || assigned)); |
| Issues and pull requests replace intents and attempts | 292 | |
| 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 attempts | 295 | 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 domains | 316 | <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 rights | 317 | {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 attempts | 323 | <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 bar | 327 | <LabelChip key={name} name={name} color={labels.find((label) => label.name === name)?.color} /> |
| Issues and pull requests replace intents and attempts | 328 | ))} |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 329 | {open && issue.agent && <Assignee agent={issue.agent} />} |
| Issues and pull requests replace intents and attempts | 330 | </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 shell | 355 | <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} /> |
| Issues and pull requests replace intents and attempts | 356 | </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-agent | 372 | Put g1t on it, or point your own agent at{" "} |
| Issues and pull requests replace intents and attempts | 373 | <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 shell | 386 | <div className="space-y-4"> |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 387 | <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 bar | 394 | <CommentForm author={viewer?.username ?? null} resetKey={comments.length} handles={loaderData.handles} /> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 395 | {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 shell | 400 | 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 shell | 403 | Close issue |
| Merge branch 'worktree-agent-a985c01f43538af01' | 404 | </SubmitButton> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 405 | </> |
| 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 shell | 408 | Reopen issue |
| Merge branch 'worktree-agent-a985c01f43538af01' | 409 | </SubmitButton> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 410 | )} |
| 411 | </Form> | |
| 412 | )} | |
| 413 | </div> | |
| Issues and pull requests replace intents and attempts | 414 | </div> |
| 415 | <div className="mt-2"> | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 416 | {/* 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 attempts | 418 | </div> |
| 419 | </div> | |
| 420 | ||
| 421 | <aside className="space-y-6"> | |
| Integrations: your own model provider, alerts that open issues, tickets agents read | 422 | {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 shell | 450 | <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-agent | 461 | <span className="block font-mono text-xs font-medium">g1t</span> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 462 | <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 shell | 469 | </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-agent | 484 | <span className="block font-mono text-xs font-medium">g1t</span> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 485 | <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-agent | 493 | {!open && resolver && resolver.agent === "g1t" && ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 494 | <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 shell | 500 | <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-agent | 501 | <span className="block font-mono text-xs font-medium">g1t</span> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 502 | <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-agent | 511 | !(!open && resolver?.agent === "g1t") && ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 512 | <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 attempts | 530 | </p> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 531 | )} |
| 532 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 533 | {open && agentsEnabled && can.run && !assigned && !issue.queued && ( |
| Issues and pull requests replace intents and attempts | 534 | <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 shell | 538 | <Sparkles size={14} /> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 539 | Assign to g1t |
| 540 | </SubmitButton> | |
| Issues and pull requests replace intents and attempts | 541 | </div> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 542 | <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 look | 558 | <ComputeNote note={loaderData.computeNote} /> |
| Merge branch 'worktree-agent-a985c01f43538af01' | 559 | {runError && <ErrorText>{actionData.error}</ErrorText>} |
| Issues and pull requests replace intents and attempts | 560 | </Form> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 561 | )} |
| 562 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 563 | {open && !agentsEnabled && can.run && !assigned && !issue.queued && ( |
| New workspaces are told to connect a model, before anything fails | 564 | // 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-agent | 568 | g1t |
| New workspaces are told to connect a model, before anything fails | 569 | </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-agent | 571 | 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 look | 572 | 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 fails | 573 | </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 look | 583 | {viewer && can.triage && open && ( |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 584 | <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 shell | 594 | Assign yourself |
| Merge branch 'worktree-agent-a985c01f43538af01' | 595 | </SubmitButton> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 596 | </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 shell | 607 | Save assignees |
| Merge branch 'worktree-agent-a985c01f43538af01' | 608 | </SubmitButton> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 609 | </Form> |
| 610 | </details> | |
| 611 | </div> | |
| 612 | )} | |
| 613 | </section> | |
| Issues and pull requests replace intents and attempts | 614 | |
| An issue being worked on no longer offers Bring your own agent beside the pull request doing it | 615 | {/* 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 attempts | 617 | <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 attempts | 636 | </div> |
| Pull requests from branches | 637 | <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 attempts | 647 | </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 bar | 659 | <LabelsBox |
| 660 | labels={labels} | |
| 661 | chosen={issue.labels} | |
| 662 | canEdit={loaderData.canManage} | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 663 | 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 bar | 665 | /> |
| 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 attempts | 672 | |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 673 | {viewer && ( |
| 674 | <SubscriptionBox action={`${base}/notifications`} number={issue.number} kind="issue" subscription={loaderData.subscription} /> | |
| 675 | )} | |
| Issues and pull requests replace intents and attempts | 676 | </aside> |
| 677 | </div> | |
| 678 | ); | |
| 679 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.