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"; |
| 2 | import { Bot, GitCommitHorizontal, GitMerge, Play, Sparkles, Terminal } from "lucide-react"; | |
| 3 | import { useEffect } from "react"; | |
| 4 | import { Form, Link, redirect, useNavigation, useRevalidator } from "react-router"; | |
| 5 | ||
| 6 | import type { Pull } from "@g1t/contracts"; | |
| 7 | ||
| 8 | import type { Route } from "./+types/issue"; | |
| 9 | import { Markdown } from "../../components/markdown"; | |
| 10 | import { | |
| 11 | Avatar, | |
| 12 | Button, | |
| 13 | CopyLine, | |
| 14 | EmptyState, | |
| 15 | ErrorText, | |
| 16 | Input, | |
| 17 | Textarea, | |
| 18 | TimeAgo, | |
| 19 | } from "../../components/ui"; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 20 | import { CheckBadge } from "../../components/checks"; |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 21 | import { |
| 22 | Assignee, | |
| 23 | AssigneeStack, | |
| 24 | ChangeSize, | |
| 25 | CommentForm, | |
| 26 | CommentList, | |
| 27 | PeoplePicker, | |
| 28 | IssueState, | |
| 29 | Label, | |
| 30 | PullIcon, | |
| 31 | plainText, | |
| 32 | } from "../../components/work"; | |
| 33 | import { identity, work } from "../../lib/services.server"; | |
| 34 | import { assertSameOrigin, getViewer, requireUser, roleIn } from "../../lib/session.server"; | |
| Issues and pull requests replace intents and attempts | 35 | |
| 36 | const REFRESH_MS = 4000; | |
| 37 | ||
| 38 | export function meta({ loaderData, params }: Route.MetaArgs) { | |
| 39 | const title = loaderData ? `${loaderData.issue.title} · Issue #${loaderData.issue.number} · ` : ""; | |
| 40 | return [{ title: `${title}${params.owner}/${params.repo} · g1t` }]; | |
| 41 | } | |
| 42 | ||
| 43 | export async function loader({ params, context }: Route.LoaderArgs) { | |
| 44 | const viewer = getViewer(context); | |
| 45 | const path = { namespace: params.owner, name: params.repo }; | |
| 46 | const number = Number(params.number); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 47 | // At once: none of these depends on another. |
| 48 | const [found, labels, agentsEnabled, members] = await Promise.all([ | |
| 49 | work.getIssue(path, number, viewer), | |
| 50 | work.listLabels(path, viewer), | |
| g1t's agents only for listed workspaces, whatever the state of billing | 51 | env.RUNNER.enabled(viewer, path), |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 52 | // A member picks assignees from the workspace's people. |
| 53 | roleIn(viewer, params.owner) ? identity.listMembers(params.owner, viewer) : null, | |
| 54 | ]); | |
| Issues and pull requests replace intents and attempts | 55 | if (!found.ok) { |
| 56 | // Issues and pull requests share numbers; this one may be a pull request. | |
| 57 | const pull = await work.getPull(path, number, viewer); | |
| 58 | if (pull.ok) throw redirect(`/${params.owner}/${params.repo}/pull/${number}`); | |
| 59 | throw new Response("Issue not found.", { status: 404 }); | |
| 60 | } | |
| 61 | const { issue } = found.value; | |
| 62 | return { | |
| 63 | ...found.value, | |
| 64 | viewer, | |
| 65 | labels: labels.ok ? labels.value : [], | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 66 | agentsEnabled, |
| 67 | members: members?.ok ? members.value.map((member) => member.username) : [], | |
| Issues and pull requests replace intents and attempts | 68 | // The author and members of the workspace can change an issue. |
| 69 | canManage: | |
| 70 | viewer != null && | |
| 71 | (viewer.id === issue.author.id || | |
| 72 | (viewer.workspaces ?? []).some((membership) => membership.slug === params.owner)), | |
| 73 | }; | |
| 74 | } | |
| 75 | ||
| 76 | export async function action({ request, params, context }: Route.ActionArgs) { | |
| 77 | assertSameOrigin(request); | |
| 78 | const user = requireUser(context, request); | |
| 79 | const form = await request.formData(); | |
| 80 | const path = { namespace: params.owner, name: params.repo }; | |
| 81 | const number = Number(params.number); | |
| 82 | ||
| 83 | switch (form.get("action")) { | |
| 84 | case "run-hosted": { | |
| 85 | const result = await env.RUNNER.run(user, path, number, { | |
| 86 | instructions: String(form.get("instructions") ?? ""), | |
| 87 | }); | |
| 88 | return result.ok ? null : { error: result.error.message }; | |
| 89 | } | |
| 90 | case "open-pull": { | |
| 91 | const result = await work.openPull(user, path, { | |
| 92 | issue: number, | |
| 93 | agent: String(form.get("agent") ?? ""), | |
| 94 | runtime: "external", | |
| 95 | }); | |
| 96 | if (!result.ok) return { error: result.error.message }; | |
| 97 | throw redirect(`/${params.owner}/${params.repo}/pull/${result.value.number}`); | |
| 98 | } | |
| 99 | case "comment": { | |
| Acceptance checks in sandboxes, line comments and review verdicts | 100 | const result = await work.addComment(user, path, number, { |
| 101 | body: String(form.get("body") ?? ""), | |
| 102 | }); | |
| Issues and pull requests replace intents and attempts | 103 | return result.ok ? null : { error: result.error.message }; |
| 104 | } | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 105 | case "assign": { |
| 106 | const result = await work.updateIssue(user, path, number, { | |
| 107 | assignees: [ | |
| 108 | ...form.getAll("assignee").map(String), | |
| 109 | ...String(form.get("others") ?? "").split(/[\s,]+/), | |
| 110 | ], | |
| 111 | }); | |
| 112 | return result.ok ? null : { error: result.error.message }; | |
| 113 | } | |
| Issues and pull requests replace intents and attempts | 114 | case "labels": { |
| 115 | const result = await work.updateIssue(user, path, number, { | |
| 116 | labels: [ | |
| 117 | ...form.getAll("label").map(String), | |
| 118 | ...String(form.get("labels") ?? "").split(","), | |
| 119 | ], | |
| 120 | }); | |
| 121 | return result.ok ? null : { error: result.error.message }; | |
| 122 | } | |
| 123 | case "reopen": { | |
| 124 | const result = await work.reopenIssue(user, path, number); | |
| 125 | return result.ok ? null : { error: result.error.message }; | |
| 126 | } | |
| 127 | default: { | |
| 128 | const result = await work.closeIssue( | |
| 129 | user, | |
| 130 | path, | |
| 131 | number, | |
| 132 | form.get("action") === "close-not-planned" ? "not_planned" : "completed", | |
| 133 | ); | |
| 134 | return result.ok ? null : { error: result.error.message }; | |
| 135 | } | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | /** What became of a pull request, in a few words. */ | |
| 140 | function outcome(pull: Pull): string { | |
| 141 | if (pull.status === "merged") return `Merged by ${pull.mergedBy ?? "someone"}`; | |
| 142 | if (pull.supersededBy != null) return `Closed · #${pull.supersededBy} was merged instead`; | |
| 143 | if (pull.status === "closed") return "Closed without merging"; | |
| 144 | if (pull.status === "draft") return "Draft · in progress"; | |
| 145 | return "Ready for review"; | |
| 146 | } | |
| 147 | ||
| 148 | function PullRow({ pull, base }: { pull: Pull; base: string }) { | |
| 149 | const merged = pull.status === "merged"; | |
| 150 | return ( | |
| 151 | <li> | |
| 152 | <Link | |
| 153 | to={`${base}/pull/${pull.number}`} | |
| 154 | className={`block rounded-xl border p-4 transition-colors ${ | |
| 155 | merged | |
| 156 | ? "border-merged/40 bg-merged/5 hover:border-merged/70" | |
| 157 | : "border-line bg-surface hover:border-line-strong" | |
| 158 | } ${pull.status === "closed" ? "opacity-70" : ""}`} | |
| 159 | > | |
| 160 | <div className="flex flex-wrap items-center gap-x-3 gap-y-1"> | |
| 161 | <PullIcon status={pull.status} /> | |
| 162 | <span className="font-medium"> | |
| 163 | {pull.title} <span className="font-normal text-faint">#{pull.number}</span> | |
| 164 | </span> | |
| 165 | {pull.status === "draft" && ( | |
| 166 | <span className="size-1.5 animate-pulse rounded-full bg-accent" /> | |
| 167 | )} | |
| 168 | <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 | 169 | <ChangeSize files={pull.files} /> |
| Acceptance checks in sandboxes, line comments and review verdicts | 170 | <CheckBadge status={pull.checkStatus} /> |
| Issues and pull requests replace intents and attempts | 171 | <span className="flex items-center gap-1 font-mono"> |
| 172 | <GitCommitHorizontal size={13} /> | |
| 173 | {pull.headCommit?.slice(0, 7) ?? "no commits"} | |
| 174 | </span> | |
| 175 | <TimeAgo at={pull.updatedAt} /> | |
| 176 | </span> | |
| 177 | </div> | |
| 178 | <p className="mt-1.5 flex flex-wrap items-center gap-x-2 text-xs text-muted"> | |
| 179 | <span className={merged ? "font-medium text-merged" : ""}>{outcome(pull)}</span> | |
| 180 | <span className="text-faint">·</span> | |
| 181 | <span className="flex items-center gap-1 font-mono"> | |
| 182 | <Bot size={12} /> | |
| 183 | {pull.agent} | |
| 184 | </span> | |
| 185 | {pull.runtime === "hosted" && <span className="text-faint">on g1t</span>} | |
| 186 | <span className="text-faint">· opened by {pull.author.username}</span> | |
| 187 | </p> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 188 | {pull.body && ( |
| 189 | <p className="mt-2 line-clamp-2 text-sm text-muted">{plainText(pull.body)}</p> | |
| 190 | )} | |
| Issues and pull requests replace intents and attempts | 191 | </Link> |
| 192 | </li> | |
| 193 | ); | |
| 194 | } | |
| 195 | ||
| 196 | export default function IssuePage({ loaderData, actionData, params }: Route.ComponentProps) { | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 197 | const { issue, pulls, comments, viewer, labels, agentsEnabled, members, canManage } = loaderData; |
| Issues and pull requests replace intents and attempts | 198 | |
| 199 | // Follow agents at work without a manual reload. | |
| 200 | const revalidator = useRevalidator(); | |
| 201 | const navigation = useNavigation(); | |
| Acceptance checks in sandboxes, line comments and review verdicts | 202 | const running = pulls.some( |
| 203 | (pull) => | |
| 204 | pull.status === "draft" || | |
| 205 | pull.checkStatus === "queued" || | |
| 206 | pull.checkStatus === "running", | |
| 207 | ); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 208 | // The pull request g1t's agent has in progress for this issue, if any. |
| 209 | const assigned = [...pulls] | |
| 210 | .reverse() | |
| 211 | .find( | |
| 212 | (pull) => | |
| 213 | pull.agent === "g1t-agent" && | |
| 214 | pull.runtime === "hosted" && | |
| 215 | (pull.status === "draft" || pull.status === "open"), | |
| 216 | ); | |
| Issues and pull requests replace intents and attempts | 217 | useEffect(() => { |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 218 | if (!running && !assigned) return; |
| Issues and pull requests replace intents and attempts | 219 | const timer = setInterval(() => { |
| 220 | if (document.visibilityState === "visible") revalidator.revalidate(); | |
| 221 | }, REFRESH_MS); | |
| 222 | return () => clearInterval(timer); | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 223 | }, [running, assigned, revalidator]); |
| Issues and pull requests replace intents and attempts | 224 | |
| 225 | const starting = navigation.formData?.get("action") === "run-hosted"; | |
| 226 | const base = `/${params.owner}/${params.repo}`; | |
| 227 | const open = issue.state === "open"; | |
| 228 | const reference = `${params.owner}/${params.repo}#${issue.number}`; | |
| 229 | const resolver = pulls.find((pull) => pull.number === issue.resolvedBy); | |
| 230 | // The merged pull request first, then the ones still in play, then the rest. | |
| 231 | const rank = { merged: 0, open: 1, draft: 2, closed: 3 } as const; | |
| 232 | const ordered = [...pulls].sort( | |
| 233 | (a, b) => rank[a.status] - rank[b.status] || a.number - b.number, | |
| 234 | ); | |
| 235 | ||
| 236 | return ( | |
| 237 | <div className="grid gap-8 lg:grid-cols-[1fr_19rem]"> | |
| 238 | <div className="min-w-0"> | |
| 239 | <h2 className="text-2xl font-semibold tracking-tight text-balance"> | |
| 240 | {issue.title} <span className="font-normal text-faint">#{issue.number}</span> | |
| 241 | </h2> | |
| 242 | <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-sm text-muted"> | |
| 243 | <IssueState issue={issue} /> | |
| 244 | <span className="flex items-center gap-2"> | |
| 245 | <Avatar name={issue.author.username} size={18} /> | |
| 246 | <span> | |
| 247 | <span className="font-medium text-fg">{issue.author.username}</span> opened this{" "} | |
| 248 | <TimeAgo at={issue.createdAt} /> | |
| 249 | </span> | |
| 250 | </span> | |
| 251 | {issue.labels.map((name) => ( | |
| 252 | <Label key={name} name={name} /> | |
| 253 | ))} | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 254 | {open && issue.agent && <Assignee agent={issue.agent} />} |
| Issues and pull requests replace intents and attempts | 255 | </div> |
| 256 | ||
| 257 | {issue.resolvedBy != null && ( | |
| 258 | <Link | |
| 259 | to={`${base}/pull/${issue.resolvedBy}`} | |
| 260 | 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" | |
| 261 | > | |
| 262 | <GitMerge size={18} className="shrink-0 text-merged" /> | |
| 263 | <span> | |
| 264 | Resolved by{" "} | |
| 265 | <span className="font-medium"> | |
| 266 | {resolver?.title ?? "pull request"} #{issue.resolvedBy} | |
| 267 | </span> | |
| 268 | {resolver?.mergedBy && ( | |
| 269 | <span className="text-muted"> | |
| 270 | , merged by {resolver.mergedBy}{" "} | |
| 271 | {resolver.mergedAt && <TimeAgo at={resolver.mergedAt} />} | |
| 272 | </span> | |
| 273 | )} | |
| 274 | </span> | |
| 275 | </Link> | |
| 276 | )} | |
| 277 | ||
| 278 | {issue.body && ( | |
| 279 | <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 | 280 | <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} /> |
| Issues and pull requests replace intents and attempts | 281 | </div> |
| 282 | )} | |
| 283 | ||
| 284 | <div className="mt-10 flex items-baseline justify-between"> | |
| 285 | <h3 className="font-semibold tracking-tight">Pull requests</h3> | |
| 286 | <p className="text-sm text-muted"> | |
| 287 | {pulls.length === 0 | |
| 288 | ? "None yet" | |
| 289 | : `${pulls.length} for this issue${ | |
| 290 | pulls.some((pull) => pull.status === "merged") ? "" : ", none merged" | |
| 291 | }`} | |
| 292 | </p> | |
| 293 | </div> | |
| 294 | <div className="mt-3"> | |
| 295 | {pulls.length === 0 ? ( | |
| 296 | <EmptyState title="Nobody has worked on this yet"> | |
| 297 | Put g1t agents on it, or point your own agent at{" "} | |
| 298 | <code className="font-mono">{reference}</code>. | |
| 299 | </EmptyState> | |
| 300 | ) : ( | |
| 301 | <ol className="space-y-3"> | |
| 302 | {ordered.map((pull) => ( | |
| 303 | <PullRow key={pull.id} pull={pull} base={base} /> | |
| 304 | ))} | |
| 305 | </ol> | |
| 306 | )} | |
| 307 | </div> | |
| 308 | ||
| 309 | <h3 className="mt-10 font-semibold tracking-tight">Discussion</h3> | |
| 310 | <div className="mt-3"> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 311 | <div className="space-y-4"> |
| 312 | <CommentList comments={comments} base={base} /> | |
| 313 | <CommentForm author={viewer?.username ?? null} resetKey={comments.length} /> | |
| 314 | {canManage && ( | |
| 315 | <Form method="post" className="flex flex-wrap justify-end gap-2"> | |
| 316 | {open ? ( | |
| 317 | <> | |
| 318 | <Button variant="quiet" type="submit" name="action" value="close-not-planned"> | |
| 319 | Close as not planned | |
| 320 | </Button> | |
| 321 | <Button variant="quiet" type="submit" name="action" value="close-completed"> | |
| 322 | Close issue | |
| 323 | </Button> | |
| 324 | </> | |
| 325 | ) : ( | |
| 326 | <Button variant="quiet" type="submit" name="action" value="reopen"> | |
| 327 | Reopen issue | |
| 328 | </Button> | |
| 329 | )} | |
| 330 | </Form> | |
| 331 | )} | |
| 332 | </div> | |
| Issues and pull requests replace intents and attempts | 333 | </div> |
| 334 | <div className="mt-2"> | |
| 335 | <ErrorText>{actionData?.error}</ErrorText> | |
| 336 | </div> | |
| 337 | </div> | |
| 338 | ||
| 339 | <aside className="space-y-6"> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 340 | <section> |
| 341 | <h3 className="text-sm font-medium">Assignees</h3> | |
| 342 | <ul className="mt-2 space-y-1.5 text-sm"> | |
| 343 | {open && assigned && ( | |
| 344 | <li> | |
| 345 | <Link | |
| 346 | to={`${base}/pull/${assigned.number}`} | |
| 347 | 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" | |
| 348 | > | |
| 349 | <Sparkles size={15} className="shrink-0 text-accent" /> | |
| 350 | <span className="min-w-0 grow"> | |
| 351 | <span className="block font-mono text-xs font-medium">g1t-agent</span> | |
| 352 | <span className="block truncate text-xs text-muted"> | |
| 353 | {assigned.status === "draft" | |
| 354 | ? "Making the change" | |
| 355 | : "Seeing it through checks and review"}{" "} | |
| 356 | · #{assigned.number} | |
| 357 | </span> | |
| 358 | </span> | |
| 359 | <span className="size-1.5 shrink-0 animate-pulse rounded-full bg-accent" /> | |
| 360 | </Link> | |
| 361 | </li> | |
| 362 | )} | |
| 363 | {issue.assignees.map((name) => ( | |
| 364 | <li key={name} className="flex items-center gap-2 px-1"> | |
| 365 | <Avatar name={name} size={20} /> | |
| 366 | <span className="grow truncate font-mono text-xs">{name}</span> | |
| 367 | </li> | |
| 368 | ))} | |
| 369 | {open && issue.queued && !assigned && ( | |
| 370 | <li className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2"> | |
| 371 | <Sparkles size={15} className="shrink-0 text-faint" /> | |
| 372 | <span className="min-w-0 grow"> | |
| 373 | <span className="block font-mono text-xs font-medium">g1t-agent</span> | |
| 374 | <span className="block text-xs text-muted"> | |
| 375 | {issue.blockedBy.length > 0 | |
| 376 | ? "Queued. Starts when what this depends on has merged." | |
| 377 | : "Queued. Starts as soon as there is room."} | |
| 378 | </span> | |
| 379 | </span> | |
| 380 | </li> | |
| 381 | )} | |
| 382 | {!open && resolver && resolver.agent === "g1t-agent" && ( | |
| 383 | <li> | |
| 384 | <Link | |
| 385 | to={`${base}/pull/${resolver.number}`} | |
| 386 | className="flex items-center gap-2 rounded-lg border border-line bg-surface px-2.5 py-2 transition-colors hover:border-line-strong" | |
| 387 | > | |
| 388 | <Sparkles size={15} className="shrink-0 text-merged" /> | |
| 389 | <span className="min-w-0 grow"> | |
| 390 | <span className="block font-mono text-xs font-medium">g1t-agent</span> | |
| 391 | <span className="block truncate text-xs text-muted"> | |
| 392 | Resolved it with #{resolver.number} | |
| 393 | </span> | |
| 394 | </span> | |
| 395 | </Link> | |
| 396 | </li> | |
| 397 | )} | |
| 398 | {issue.assignees.length === 0 && | |
| 399 | !(open && (assigned || issue.queued)) && | |
| 400 | !(!open && resolver?.agent === "g1t-agent") && ( | |
| 401 | <li className="px-1 text-xs text-faint">No one yet.</li> | |
| 402 | )} | |
| 403 | </ul> | |
| 404 | {issue.blockedBy.length > 0 && ( | |
| 405 | <p className="mt-3 text-xs text-muted"> | |
| 406 | Depends on{" "} | |
| 407 | {issue.blockedBy.map((number, index) => ( | |
| 408 | <span key={number}> | |
| 409 | {index > 0 && ", "} | |
| 410 | <Link | |
| 411 | to={`${base}/issues/${number}`} | |
| 412 | className="font-medium text-fg hover:underline" | |
| 413 | > | |
| 414 | #{number} | |
| 415 | </Link> | |
| 416 | </span> | |
| 417 | ))} | |
| 418 | {open ? ", which has to merge first." : "."} | |
| Issues and pull requests replace intents and attempts | 419 | </p> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 420 | )} |
| 421 | ||
| 422 | {open && agentsEnabled && !assigned && !issue.queued && ( | |
| Issues and pull requests replace intents and attempts | 423 | <Form method="post" className="mt-3 space-y-2"> |
| 424 | <input type="hidden" name="action" value="run-hosted" /> | |
| 425 | <div className="*:w-full"> | |
| 426 | <Button variant="accent" type="submit" disabled={starting}> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 427 | <Sparkles size={14} /> |
| 428 | {starting ? "Starting a sandbox…" : "Assign to g1t agent"} | |
| Issues and pull requests replace intents and attempts | 429 | </Button> |
| 430 | </div> | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 431 | <details> |
| 432 | <summary className="cursor-pointer text-xs text-faint hover:text-fg"> | |
| 433 | Add guidance for this run | |
| 434 | </summary> | |
| 435 | <div className="mt-2"> | |
| 436 | <Textarea | |
| 437 | name="instructions" | |
| 438 | rows={2} | |
| 439 | placeholder="On top of the issue's description" | |
| 440 | /> | |
| 441 | </div> | |
| 442 | </details> | |
| 443 | <p className="text-xs text-muted"> | |
| 444 | It opens a pull request and sees it through checks, a review by another | |
| 445 | agent and fixes. You get it back ready to merge. | |
| 446 | </p> | |
| Issues and pull requests replace intents and attempts | 447 | </Form> |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 448 | )} |
| 449 | ||
| 450 | {viewer && canManage && open && ( | |
| 451 | <div className="mt-3 space-y-2"> | |
| 452 | {!issue.assignees.includes(viewer.username) && ( | |
| 453 | <Form method="post"> | |
| 454 | <input type="hidden" name="action" value="assign" /> | |
| 455 | {issue.assignees.map((name) => ( | |
| 456 | <input key={name} type="hidden" name="assignee" value={name} /> | |
| 457 | ))} | |
| 458 | <input type="hidden" name="assignee" value={viewer.username} /> | |
| 459 | <div className="*:w-full"> | |
| 460 | <Button variant="quiet" type="submit"> | |
| 461 | Assign yourself | |
| 462 | </Button> | |
| 463 | </div> | |
| 464 | </Form> | |
| 465 | )} | |
| 466 | <details className="group"> | |
| 467 | <summary className="cursor-pointer text-xs text-faint hover:text-fg"> | |
| 468 | Assign people | |
| 469 | </summary> | |
| 470 | <Form method="post" className="mt-2 space-y-2" key={issue.assignees.join()}> | |
| 471 | <input type="hidden" name="action" value="assign" /> | |
| 472 | <PeoplePicker name="assignee" members={members} chosen={issue.assignees} /> | |
| 473 | <Button variant="quiet" type="submit"> | |
| 474 | Save assignees | |
| 475 | </Button> | |
| 476 | </Form> | |
| 477 | </details> | |
| 478 | </div> | |
| 479 | )} | |
| 480 | </section> | |
| Issues and pull requests replace intents and attempts | 481 | |
| 482 | {open && ( | |
| 483 | <section className="rounded-xl border border-line bg-surface p-4"> | |
| 484 | <h3 className="text-sm font-medium">Bring your own agent</h3> | |
| 485 | <p className="mt-1 text-xs text-muted"> | |
| 486 | With g1t connected to your agent, ask it to work on this issue. | |
| 487 | </p> | |
| 488 | <div className="mt-3"> | |
| 489 | <CopyLine text={reference} /> | |
| 490 | </div> | |
| 491 | {viewer ? ( | |
| 492 | <Form method="post" className="mt-4 space-y-2 border-t border-line pt-4"> | |
| 493 | <input type="hidden" name="action" value="open-pull" /> | |
| 494 | <p className="text-xs text-muted"> | |
| 495 | Or open a draft pull request yourself and get a fork to push to. | |
| 496 | </p> | |
| 497 | <Input name="agent" placeholder="Who is working, e.g. claude-code" maxLength={60} /> | |
| 498 | <div className="*:w-full"> | |
| 499 | <Button type="submit">Open pull request</Button> | |
| 500 | </div> | |
| Pull requests from branches | 501 | <p className="text-xs text-muted"> |
| 502 | Already pushed a branch?{" "} | |
| 503 | <Link | |
| 504 | to={`${base}/pulls/new?issue=${issue.number}`} | |
| 505 | className="text-fg underline underline-offset-4" | |
| 506 | > | |
| 507 | Open a pull request from it | |
| 508 | </Link> | |
| 509 | . | |
| 510 | </p> | |
| Issues and pull requests replace intents and attempts | 511 | </Form> |
| 512 | ) : ( | |
| 513 | <p className="mt-4 border-t border-line pt-4 text-sm text-muted"> | |
| 514 | <Link to="/login" className="text-fg underline underline-offset-4"> | |
| 515 | Sign in | |
| 516 | </Link>{" "} | |
| 517 | to open a pull request. | |
| 518 | </p> | |
| 519 | )} | |
| 520 | </section> | |
| 521 | )} | |
| 522 | ||
| 523 | {issue.checks.length > 0 && ( | |
| 524 | <section> | |
| 525 | <h3 className="text-sm font-medium">Acceptance checks</h3> | |
| 526 | <ul className="mt-2 space-y-1.5"> | |
| 527 | {issue.checks.map((check) => ( | |
| 528 | <li | |
| 529 | key={check} | |
| 530 | className="flex items-center gap-2 rounded-md border border-line bg-surface px-2.5 py-1.5 font-mono text-xs" | |
| 531 | > | |
| 532 | <Terminal size={13} className="shrink-0 text-faint" /> | |
| 533 | <span className="truncate">{check}</span> | |
| 534 | </li> | |
| 535 | ))} | |
| 536 | </ul> | |
| 537 | </section> | |
| 538 | )} | |
| 539 | ||
| 540 | {canManage && ( | |
| 541 | <details className="group"> | |
| 542 | <summary className="cursor-pointer list-none text-sm font-medium"> | |
| 543 | Labels <span className="text-xs font-normal text-faint group-open:hidden">Edit</span> | |
| 544 | </summary> | |
| 545 | <Form method="post" className="mt-3 space-y-3" key={issue.labels.join()}> | |
| 546 | <input type="hidden" name="action" value="labels" /> | |
| 547 | <div className="flex flex-wrap gap-x-3 gap-y-2"> | |
| 548 | {labels.map((name) => ( | |
| 549 | <label key={name} className="flex cursor-pointer items-center gap-1.5"> | |
| 550 | <input | |
| 551 | type="checkbox" | |
| 552 | name="label" | |
| 553 | value={name} | |
| 554 | defaultChecked={issue.labels.includes(name)} | |
| 555 | className="accent-accent" | |
| 556 | /> | |
| 557 | <Label name={name} /> | |
| 558 | </label> | |
| 559 | ))} | |
| 560 | </div> | |
| 561 | <Input name="labels" placeholder="New labels, comma separated" /> | |
| 562 | <Button variant="quiet" type="submit"> | |
| 563 | Save labels | |
| 564 | </Button> | |
| 565 | </Form> | |
| 566 | </details> | |
| 567 | )} | |
| 568 | ||
| 569 | </aside> | |
| 570 | </div> | |
| 571 | ); | |
| 572 | } |