Merge branch 'g1t-takes-issues' into ship-42-43
36 files+481−2320 files not counted0/36 viewed
| 506 | 506 | actor: actor(), | |
| 507 | 507 | repo, | |
| 508 | 508 | issue, | |
| 509 | + | pull: None, | |
| 510 | + | asked: None, | |
| 509 | 511 | title: text(input, "title"), | |
| 510 | 512 | body: text(input, "body"), | |
| 511 | 513 | instruction: text(input, "instruction"), |
| 359 | 359 | Op::AddComment, | |
| 360 | 360 | Op::EditComment, | |
| 361 | 361 | Op::DeleteComment, | |
| 362 | + | Op::React, | |
| 362 | 363 | Op::ListIssueLabels, | |
| 363 | 364 | Op::AddIssueLabels, | |
| 364 | 365 | Op::SetIssueLabels, | |
| ⋯ | |||
| 734 | 735 | Op::AddComment => "Add a comment", | |
| 735 | 736 | Op::EditComment => "Edit a comment", | |
| 736 | 737 | Op::DeleteComment => "Delete a comment", | |
| 738 | + | Op::React => "React with an emoji", | |
| 737 | 739 | Op::ReviewPullRequest => "Review a pull request", | |
| 738 | 740 | Op::ListPullRequests => "List pull requests", | |
| 739 | 741 | Op::GetPullRequest => "Get a pull request", | |
| 190 | 190 | AddComment, | |
| 191 | 191 | EditComment, | |
| 192 | 192 | DeleteComment, | |
| 193 | + | React, | |
| 193 | 194 | ReviewPullRequest, | |
| 194 | 195 | ListPullRequests, | |
| 195 | 196 | GetPullRequest, | |
| ⋯ | |||
| 811 | 812 | } | |
| 812 | 813 | ||
| 813 | 814 | impl Op { | |
| 814 | − | pub const ALL: [Op; 405] = [ | |
| 815 | + | pub const ALL: [Op; 406] = [ | |
| 815 | 816 | Op::Whoami, | |
| 816 | 817 | Op::GetWorkspace, | |
| 817 | 818 | Op::CreateWorkspace, | |
| ⋯ | |||
| 885 | 886 | Op::AddComment, | |
| 886 | 887 | Op::EditComment, | |
| 887 | 888 | Op::DeleteComment, | |
| 889 | + | Op::React, | |
| 888 | 890 | Op::ReviewPullRequest, | |
| 889 | 891 | Op::ListPullRequests, | |
| 890 | 892 | Op::GetPullRequest, | |
| ⋯ | |||
| 1299 | 1301 | Op::AddComment => "add_comment", | |
| 1300 | 1302 | Op::EditComment => "edit_comment", | |
| 1301 | 1303 | Op::DeleteComment => "delete_comment", | |
| 1304 | + | Op::React => "react", | |
| 1302 | 1305 | Op::ReviewPullRequest => "review_pull_request", | |
| 1303 | 1306 | Op::ListPullRequests => "list_pull_requests", | |
| 1304 | 1307 | Op::GetPullRequest => "get_pull_request", | |
| ⋯ | |||
| 1663 | 1666 | Op::DeleteComment => { | |
| 1664 | 1667 | "Delete a comment on an issue or a pull request, named by comment_id. Its author may delete it, and so may anyone with the Maintain role or higher. A review that approved or requested changes cannot be deleted, only edited, and notes of what happened cannot be deleted. This cannot be undone. Publishes comment.deleted with the comment as it was." | |
| 1665 | 1668 | } | |
| 1669 | + | Op::React => { | |
| 1670 | + | "React to an issue or a pull request with an emoji: to its description, or with comment_id to one of its comments. content is one of +1, -1, laugh, hooray, confused, heart, rocket and eyes; remove true takes your reaction back. Anyone who can read the repository may. Answers with the reactions it has now, each with its count and the usernames of who reacted (`g1t` for g1t, which reacts eyes to what it was asked to take on). get_issue and get_pull_request give each comment's reactions, and the description's as reactions." | |
| 1671 | + | } | |
| 1666 | 1672 | Op::ReviewPullRequest => { | |
| 1667 | 1673 | "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)." | |
| 1668 | 1674 | } | |
| ⋯ | |||
| 2716 | 2722 | json!({ "repo": repo_schema(), "comment_id": comment_id_schema() }), | |
| 2717 | 2723 | &["repo", "comment_id"], | |
| 2718 | 2724 | ), | |
| 2725 | + | Op::React => object( | |
| 2726 | + | numbered(json!({ | |
| 2727 | + | "content": { | |
| 2728 | + | "type": "string", | |
| 2729 | + | "enum": ["+1", "-1", "laugh", "hooray", "confused", "heart", "rocket", "eyes"], | |
| 2730 | + | }, | |
| 2731 | + | "comment_id": { | |
| 2732 | + | "type": "string", | |
| 2733 | + | "description": "A comment on it, to react to that comment rather than to its description.", | |
| 2734 | + | }, | |
| 2735 | + | "remove": { "type": "boolean", "description": "Take your reaction back." }, | |
| 2736 | + | })), | |
| 2737 | + | &["repo", "number", "content"], | |
| 2738 | + | ), | |
| 2719 | 2739 | Op::ReviewPullRequest => object( | |
| 2720 | 2740 | numbered(json!({ | |
| 2721 | 2741 | "verdict": { "type": "string", "enum": ["approve", "request_changes"] }, | |
| ⋯ | |||
| 5238 | 5258 | Op::ClosePullRequest => pass(work, "close_pull", &pull_action()).await, | |
| 5239 | 5259 | Op::ReopenPullRequest => pass(work, "reopen_pull", &pull_action()).await, | |
| 5240 | 5260 | Op::ConvertPullRequestToDraft => pass(work, "convert_pull_to_draft", &pull_action()).await, | |
| 5261 | + | Op::React => { | |
| 5262 | + | let Some(content) = input["content"].as_str().and_then(ReactionContent::parse) else { | |
| 5263 | + | return failed(FailureCode::Invalid, "content must be one of +1, -1, laugh, hooray, confused, heart, rocket and eyes."); | |
| 5264 | + | }; | |
| 5265 | + | let asked = ReactArgs { | |
| 5266 | + | actor: actor(), | |
| 5267 | + | repo, | |
| 5268 | + | number, | |
| 5269 | + | comment_id: optional_text(input, "comment_id"), | |
| 5270 | + | content, | |
| 5271 | + | remove: input["remove"].as_bool().unwrap_or(false), | |
| 5272 | + | }; | |
| 5273 | + | pass(work, "react", &asked).await | |
| 5274 | + | } | |
| 5241 | 5275 | Op::EditComment | Op::DeleteComment => { | |
| 5242 | 5276 | let asked = CommentActionArgs { | |
| 5243 | 5277 | actor: actor(), | |
| 1604 | 1604 | }, | |
| 1605 | 1605 | "notes": "Answers `204` with no body. Its author, or someone with the Maintain role or higher. A review that approved or requested changes answers `409`: edit it instead." | |
| 1606 | 1606 | }, | |
| 1607 | + | "react": { | |
| 1608 | + | "params": { | |
| 1609 | + | "number": 12, | |
| 1610 | + | "comment_id": "cmt_01m43sv4c8e2g6j0m4q8t2x6a1", | |
| 1611 | + | "content": "heart" | |
| 1612 | + | }, | |
| 1613 | + | "response": [ | |
| 1614 | + | { "content": "heart", "count": 1, "users": ["ana"] }, | |
| 1615 | + | { "content": "eyes", "count": 1, "users": ["g1t"] } | |
| 1616 | + | ], | |
| 1617 | + | "notes": "Reacting twice the same way is one reaction. Without comment_id it reacts to the issue's or pull request's description." | |
| 1618 | + | }, | |
| 1607 | 1619 | "list_labels": { | |
| 1608 | 1620 | "response": [ | |
| 1609 | 1621 | { |
| 1046 | 1046 | ), | |
| 1047 | 1047 | route("PATCH", "/repos/:owner/:name/issues/comments/:comment_id", Op::EditComment, &[]), | |
| 1048 | 1048 | route("DELETE", "/repos/:owner/:name/issues/comments/:comment_id", Op::DeleteComment, &[]), | |
| 1049 | + | route("POST", "/repos/:owner/:name/issues/:number/reactions", Op::React, &[]), | |
| 1049 | 1050 | route( | |
| 1050 | 1051 | "GET", | |
| 1051 | 1052 | "/repos/:owner/:name/pulls", | |
| ⋯ | |||
| 1579 | 1580 | assert_eq!(input, json!({ "comment_id": "cmt_1", "repo": "acme/web" })); | |
| 1580 | 1581 | // Still an issue's comments, labels and subscription. | |
| 1581 | 1582 | assert_eq!(op("POST", "/repos/acme/web/issues/7/comments"), Op::AddComment); | |
| 1583 | + | assert_eq!(op("POST", "/repos/acme/web/issues/7/reactions"), Op::React); | |
| 1582 | 1584 | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/labels"), Op::RemoveIssueLabels); | |
| 1583 | 1585 | assert_eq!(op("DELETE", "/repos/acme/web/issues/7/subscription"), Op::DeleteThreadSubscription); | |
| 1584 | 1586 | } | |
| 165 | 165 | a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"), | |
| 166 | 166 | a("edit_comment", Op::EditComment, "Change a comment's text: your own, or any with the Maintain role"), | |
| 167 | 167 | a("delete_comment", Op::DeleteComment, "Delete a comment: your own, or any with the Maintain role"), | |
| 168 | + | a("react", Op::React, "React with an emoji to it, or with comment_id to one of its comments; remove takes it back"), | |
| 168 | 169 | a("import", Op::ImportIssue, "Open an issue from a Jira, Linear or Sentry item"), | |
| 169 | 170 | a("work", Op::Coordination(CoordinationOp::GetWork), "Everything to pick it up: done means, claims, sessions, pull requests, blockers, decisions, recent events"), | |
| 170 | 171 | a("claim", Op::Coordination(CoordinationOp::ClaimIssue), "Say you are working on it; lists who else is, and what it is blocked by"), |
| 182 | 182 | ||
| 183 | 183 | ## Sessions | |
| 184 | 184 | ||
| 185 | + | Each session that works on a pull request, or on its issue, shows in the | |
| 186 | + | conversation as a card where it started, the way a comment or a review | |
| 187 | + | does: who it is (or g1t, working for someone), where it stands, its | |
| 188 | + | branch, and its report once it ends, linked to its page. Mention `@g1t` | |
| 189 | + | in a comment to have g1t carry on with the pull request; see | |
| 190 | + | [assign g1t, or ask it in the thread](/guides/sessions/#assign-g1t-or-ask-it-in-the-thread). | |
| 191 | + | ||
| 185 | 192 | A coding agent that works on a pull request can record how it made the | |
| 186 | 193 | change, entry by entry, with the `pull_request` tool's `record_session` | |
| 187 | 194 | action. Read it on the pull request's **Session** tab, or with | |
| ⋯ | |||
| 293 | 300 | `changes.body.from`; deleting publishes `comment.deleted`, with the comment | |
| 294 | 301 | as it was in `comment`. | |
| 295 | 302 | ||
| 303 | + | ## Reactions | |
| 304 | + | ||
| 305 | + | React to an issue's or pull request's description, or to any comment on | |
| 306 | + | it, with one of eight emoji: 👍 👎 😄 🎉 😕 ❤️ 🚀 👀. Select the smiley | |
| 307 | + | under it, or a reaction already there to add or take back yours; hovering | |
| 308 | + | one says who reacted. Anyone who can read the repository may react. g1t | |
| 309 | + | reacts 👀 to what it was asked to take on. | |
| 310 | + | ||
| 311 | + | Through the API, `POST /repos/{owner}/{name}/issues/{number}/reactions` | |
| 312 | + | with `content` (`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, | |
| 313 | + | `rocket` or `eyes`), and `comment_id` to react to a comment rather than | |
| 314 | + | the description, reacts and answers with the reactions it has now; | |
| 315 | + | `remove: true` takes yours back. On the MCP server it is the `issue` | |
| 316 | + | tool's `react` action. Each comment's `reactions`, and the description's, | |
| 317 | + | are in `GET /repos/{owner}/{name}/issues/{number}` and | |
| 318 | + | `GET /repos/{owner}/{name}/pulls/{number}`: each with its `content`, | |
| 319 | + | `count` and the `users` who reacted. | |
| 320 | + | ||
| 296 | 321 | ## Commit comments | |
| 297 | 322 | ||
| 298 | 323 | You can comment on any commit: on the whole commit, under its changes on | |
| 313 | 313 | result. Whoever started it can stop it from its page. It runs on your | |
| 314 | 314 | workspace's key and Anthropic bills the account for it; g1t bills nothing. | |
| 315 | 315 | ||
| 316 | + | ## Assign g1t, or ask it in the thread | |
| 317 | + | ||
| 318 | + | You can also hand work to g1t from the issue or pull request itself, the | |
| 319 | + | way you would ask a teammate: | |
| 320 | + | ||
| 321 | + | - **Assign the issue to `g1t`**: **Assign to g1t** in the issue's | |
| 322 | + | sidebar, `g1t` among the assignees in | |
| 323 | + | [`update_issue`](/reference/api/issues/update-issue/), or | |
| 324 | + | `g1t issue edit 12 --add-assignee g1t`. | |
| 325 | + | - **Mention `@g1t` in a comment** on an issue or a pull request: | |
| 326 | + | "@g1t the button still overlaps on a phone, fix that". On a pull | |
| 327 | + | request, the session carries on with that pull request on its branch. | |
| 328 | + | On an issue, it takes the issue on. | |
| 329 | + | ||
| 330 | + | g1t reacts 👀 to what asked it, the comment or (for an assignment) the | |
| 331 | + | issue, then starts a session on the workspace's Anthropic account, as | |
| 332 | + | the person who asked, with what they wrote as its instruction. Each | |
| 333 | + | session shows in the issue's or pull request's conversation as a card, | |
| 334 | + | where it started, the way a comment or a review does: who it works for, | |
| 335 | + | where it stands, its branch, and its report when it ends, linked to its | |
| 336 | + | page. Not happy with what a session did? Say so with `@g1t` in the same | |
| 337 | + | thread. The next session is told to fix the work on the same branch and | |
| 338 | + | pull request, not start over, and `g1t work context` shows it every | |
| 339 | + | session and pull request so far. If the session you started on that | |
| 340 | + | thread is still going, your comment goes to it instead of starting | |
| 341 | + | another. | |
| 342 | + | ||
| 343 | + | Only the workspace's members can ask g1t; anyone else's `@g1t` is an | |
| 344 | + | ordinary mention. A mention in quoted text or code (`` `@g1t` ``) asks | |
| 345 | + | nothing. g1t answers one thread at most six times an hour. When it cannot | |
| 346 | + | start a session, for instance because the workspace has no Anthropic AI | |
| 347 | + | provider, it says why in the thread. On an update pull request g1t opened, | |
| 348 | + | `@g1t rebase` and the other | |
| 349 | + | [comment commands](/guides/dependency-updates/#comment-commands) still do what | |
| 350 | + | they say. | |
| 351 | + | ||
| 316 | 352 | ## Claims | |
| 317 | 353 | ||
| 318 | 354 | A claim says you are working on an issue. It is advisory: anyone may claim |
Binary or large file; its contents are not shown.
| 77 | 77 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 78 | 78 | import { RadioGroup, RadioOption } from "./ui/radio-group"; | |
| 79 | 79 | import { Textarea } from "./ui/textarea"; | |
| 80 | − | import { PersonLink } from "./work"; | |
| 80 | + | import { Markdown } from "./markdown"; | |
| 81 | + | import { PersonLink, TimelineItem } from "./work"; | |
| 82 | + | import { repoAt } from "../lib/markdown-plugins"; | |
| 81 | 83 | import { IssueIcon, PullIcon } from "./work-icons"; | |
| 82 | 84 | ||
| 83 | 85 | /** Text with each `#N` linked to that issue or pull request (the issue's address leads to a pull request too). */ | |
| ⋯ | |||
| 223 | 225 | ); | |
| 224 | 226 | } | |
| 225 | 227 | ||
| 228 | + | /** | |
| 229 | + | * A session in an issue's or pull request's conversation, where it | |
| 230 | + | * started, as a card like a comment or a review: g1t working for someone | |
| 231 | + | * (a session g1t started on the workspace's Claude, when it was assigned | |
| 232 | + | * the issue or asked in the thread), or someone working in their own | |
| 233 | + | * harness. What it is about, where it stands now, its branch, and what it | |
| 234 | + | * reported once it ended, linked to its page. | |
| 235 | + | */ | |
| 236 | + | export function TimelineSession({ session, base }: { session: WorkSession; base: string }) { | |
| 237 | + | const g1t = session.harness === "claude_managed_agents"; | |
| 238 | + | const ended = session.state === "ended"; | |
| 239 | + | return ( | |
| 240 | + | <TimelineItem | |
| 241 | + | id={session.id} | |
| 242 | + | author={g1t ? "g1t" : session.actor.username} | |
| 243 | + | bot={g1t} | |
| 244 | + | at={session.startedAt} | |
| 245 | + | action={ | |
| 246 | + | <> | |
| 247 | + | {g1t ? ( | |
| 248 | + | <span> | |
| 249 | + | {ended ? "worked on this for" : "is working on this for"}{" "} | |
| 250 | + | <PersonLink name={session.actor.username} className="font-medium text-fg hover:underline"> | |
| 251 | + | @{session.actor.username} | |
| 252 | + | </PersonLink> | |
| 253 | + | </span> | |
| 254 | + | ) : ( | |
| 255 | + | <span>{ended ? "worked on this in a session" : "is working on this in a session"}</span> | |
| 256 | + | )} | |
| 257 | + | <HarnessBadge harness={session.harness} /> | |
| 258 | + | <SessionStateBadge state={session.state} /> | |
| 259 | + | </> | |
| 260 | + | } | |
| 261 | + | aside={ | |
| 262 | + | <Link | |
| 263 | + | to={`${base}/work/sessions/${session.id}`} | |
| 264 | + | prefetch="intent" | |
| 265 | + | className="inline-flex items-center gap-1 text-xs text-faint hover:text-fg" | |
| 266 | + | > | |
| 267 | + | <Activity size={12} /> | |
| 268 | + | View session | |
| 269 | + | </Link> | |
| 270 | + | } | |
| 271 | + | > | |
| 272 | + | <Link to={`${base}/work/sessions/${session.id}`} prefetch="intent" className="font-medium hover:text-accent"> | |
| 273 | + | {sessionTitle(session)} | |
| 274 | + | </Link> | |
| 275 | + | <p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted"> | |
| 276 | + | {ended ? ( | |
| 277 | + | <span> | |
| 278 | + | Ended <TimeAgo at={session.endedAt ?? session.lastSeenAt} /> | |
| 279 | + | </span> | |
| 280 | + | ) : ( | |
| 281 | + | <span> | |
| 282 | + | Last heard from <TimeAgo at={session.lastSeenAt} /> | |
| 283 | + | </span> | |
| 284 | + | )} | |
| 285 | + | {session.branch && ( | |
| 286 | + | <span className="inline-flex min-w-0 items-center gap-1 font-mono"> | |
| 287 | + | · <GitBranch size={11} /> | |
| 288 | + | <span className="truncate">{session.branch}</span> | |
| 289 | + | </span> | |
| 290 | + | )} | |
| 291 | + | {session.pull != null && ( | |
| 292 | + | <span> | |
| 293 | + | · on{" "} | |
| 294 | + | <Link to={`${base}/pull/${session.pull}`} className="text-fg hover:text-accent"> | |
| 295 | + | #{session.pull} | |
| 296 | + | </Link> | |
| 297 | + | </span> | |
| 298 | + | )} | |
| 299 | + | {(session.subagents ?? 0) > 0 && ( | |
| 300 | + | <span> | |
| 301 | + | · {session.subagents} {session.subagents === 1 ? "subagent" : "subagents"} | |
| 302 | + | </span> | |
| 303 | + | )} | |
| 304 | + | </p> | |
| 305 | + | {ended && session.result && ( | |
| 306 | + | <div className="mt-3 border-t border-line pt-3 text-sm"> | |
| 307 | + | <Markdown source={session.result} repo={repoAt(base)} /> | |
| 308 | + | </div> | |
| 309 | + | )} | |
| 310 | + | </TimelineItem> | |
| 311 | + | ); | |
| 312 | + | } | |
| 313 | + | ||
| 226 | 314 | /** A subagent in a line: its kind, its task, where it stands, linked to its page. */ | |
| 227 | 315 | export function SubagentLine({ session, base }: { session: WorkSession; base: string }) { | |
| 228 | 316 | return ( | |
| 1 | + | import { SmilePlus } from "lucide-react"; | |
| 2 | + | import { useFetcher } from "react-router"; | |
| 3 | + | ||
| 4 | + | import { REACTIONS, type ReactionContent, type ReactionGroup } from "@g1t/contracts"; | |
| 5 | + | ||
| 6 | + | import { Button } from "./ui/button"; | |
| 7 | + | import { Hint } from "./ui/hint"; | |
| 8 | + | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 9 | + | ||
| 10 | + | const EMOJI = Object.fromEntries(REACTIONS.map((one) => [one.content, one])) as Record< | |
| 11 | + | ReactionContent, | |
| 12 | + | (typeof REACTIONS)[number] | |
| 13 | + | >; | |
| 14 | + | ||
| 15 | + | /** "ana, bo and g1t" for who reacted; "You" for the viewer. */ | |
| 16 | + | function who(users: string[], viewer: string | null) { | |
| 17 | + | const names = users.map((name) => (name === viewer ? "You" : name)); | |
| 18 | + | if (names.length <= 1) return names.join(""); | |
| 19 | + | return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`; | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | /** `groups` with the viewer's reaction added or taken back, as the page shows it before the service answers. */ | |
| 23 | + | function applied(groups: ReactionGroup[], viewer: string, content: ReactionContent, remove: boolean): ReactionGroup[] { | |
| 24 | + | const next = groups | |
| 25 | + | .map((group) => { | |
| 26 | + | if (group.content !== content) return group; | |
| 27 | + | const users = remove ? group.users.filter((name) => name !== viewer) : group.users.includes(viewer) ? group.users : [...group.users, viewer]; | |
| 28 | + | return { ...group, users, count: users.length }; | |
| 29 | + | }) | |
| 30 | + | .filter((group) => group.count > 0); | |
| 31 | + | if (!remove && !next.some((group) => group.content === content)) next.push({ content, count: 1, users: [viewer] }); | |
| 32 | + | const order = REACTIONS.map((one) => one.content); | |
| 33 | + | return next.sort((a, b) => order.indexOf(a.content) - order.indexOf(b.content)); | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | /** | |
| 37 | + | * How people (and g1t) reacted to an issue's or pull request's description | |
| 38 | + | * (no `commentId`) or to one of its comments: a pill per emoji with how | |
| 39 | + | * many, lit when the viewer is one of them, and who when hovered. Signed | |
| 40 | + | * in, a pill adds or takes back the viewer's own and the smiley offers the | |
| 41 | + | * rest; the page's action answers `react`. g1t's 👀 says it has seen what | |
| 42 | + | * it was asked and is on it. | |
| 43 | + | */ | |
| 44 | + | export function Reactions({ | |
| 45 | + | reactions, | |
| 46 | + | commentId, | |
| 47 | + | viewer, | |
| 48 | + | className = "", | |
| 49 | + | }: { | |
| 50 | + | reactions?: ReactionGroup[]; | |
| 51 | + | commentId?: string; | |
| 52 | + | /** The signed-in viewer's username, or null: then the pills only show. */ | |
| 53 | + | viewer: string | null; | |
| 54 | + | className?: string; | |
| 55 | + | }) { | |
| 56 | + | const fetcher = useFetcher(); | |
| 57 | + | let groups = reactions ?? []; | |
| 58 | + | // What the viewer just did, shown at once. | |
| 59 | + | const pending = fetcher.formData; | |
| 60 | + | if (viewer && pending?.get("action") === "react") { | |
| 61 | + | const content = String(pending.get("content")) as ReactionContent; | |
| 62 | + | if (EMOJI[content]) groups = applied(groups, viewer, content, pending.get("remove") === "1"); | |
| 63 | + | } | |
| 64 | + | if (groups.length === 0 && !viewer) return null; | |
| 65 | + | const react = (content: ReactionContent, remove: boolean) => | |
| 66 | + | fetcher.submit( | |
| 67 | + | { action: "react", comment: commentId ?? "", content, remove: remove ? "1" : "" }, | |
| 68 | + | { method: "post", preventScrollReset: true }, | |
| 69 | + | ); | |
| 70 | + | return ( | |
| 71 | + | <div className={`flex flex-wrap items-center gap-1 ${className}`} aria-label="Reactions"> | |
| 72 | + | {groups.map((group) => { | |
| 73 | + | const mine = viewer != null && group.users.includes(viewer); | |
| 74 | + | const { emoji, label } = EMOJI[group.content]; | |
| 75 | + | return ( | |
| 76 | + | <Hint key={group.content} label={`${who(group.users, viewer)} reacted with ${label.toLowerCase()}`}> | |
| 77 | + | <button | |
| 78 | + | type="button" | |
| 79 | + | aria-pressed={mine} | |
| 80 | + | aria-label={`${label}, ${group.count} ${group.count === 1 ? "reaction" : "reactions"}${mine ? ", including yours" : ""}`} | |
| 81 | + | disabled={!viewer} | |
| 82 | + | onClick={() => react(group.content, mine)} | |
| 83 | + | className={`flex h-6 items-center gap-1 rounded-full border px-1.5 text-xs font-medium tabular-nums transition-colors disabled:cursor-default ${ | |
| 84 | + | mine | |
| 85 | + | ? "border-accent/60 bg-accent/15 text-accent hover:bg-accent/25" | |
| 86 | + | : "border-line bg-surface text-muted hover:border-line-strong hover:bg-raised hover:text-fg" | |
| 87 | + | }`} | |
| 88 | + | > | |
| 89 | + | <span aria-hidden className="text-sm leading-none"> | |
| 90 | + | {emoji} | |
| 91 | + | </span> | |
| 92 | + | {group.count} | |
| 93 | + | </button> | |
| 94 | + | </Hint> | |
| 95 | + | ); | |
| 96 | + | })} | |
| 97 | + | {viewer && ( | |
| 98 | + | <Popover> | |
| 99 | + | <PopoverTrigger asChild> | |
| 100 | + | <Button | |
| 101 | + | type="button" | |
| 102 | + | aria-label="Add a reaction" | |
| 103 | + | variant="outline" | |
| 104 | + | className="h-6 rounded-full bg-surface px-1.5 text-faint hover:bg-raised" | |
| 105 | + | > | |
| 106 | + | <SmilePlus size={14} /> | |
| 107 | + | </Button> | |
| 108 | + | </PopoverTrigger> | |
| 109 | + | <PopoverContent align="start" className="w-auto p-1.5"> | |
| 110 | + | <div className="flex gap-0.5"> | |
| 111 | + | {REACTIONS.map(({ content, emoji, label }) => { | |
| 112 | + | const mine = groups.some((group) => group.content === content && group.users.includes(viewer)); | |
| 113 | + | return ( | |
| 114 | + | <Hint key={content} label={label}> | |
| 115 | + | <Button | |
| 116 | + | type="button" | |
| 117 | + | variant="ghost" | |
| 118 | + | size="icon-sm" | |
| 119 | + | aria-label={label} | |
| 120 | + | aria-pressed={mine} | |
| 121 | + | className={`text-base ${mine ? "bg-accent/15" : ""}`} | |
| 122 | + | onClick={() => react(content, mine)} | |
| 123 | + | > | |
| 124 | + | {emoji} | |
| 125 | + | </Button> | |
| 126 | + | </Hint> | |
| 127 | + | ); | |
| 128 | + | })} | |
| 129 | + | </div> | |
| 130 | + | </PopoverContent> | |
| 131 | + | </Popover> | |
| 132 | + | )} | |
| 133 | + | </div> | |
| 134 | + | ); | |
| 135 | + | } |
| 1 | 1 | import { Bot, CircleCheck, CircleSlash, Pencil, Trash2 } from "lucide-react"; | |
| 2 | − | import type { CSSProperties, ReactNode } from "react"; | |
| 2 | + | import { type CSSProperties, Fragment, type ReactNode } from "react"; | |
| 3 | 3 | import { Form, Link } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import type { AgentRef, Comment, Issue, Pull, State } from "@g1t/contracts"; | |
| ⋯ | |||
| 8 | 8 | import { chipStyle } from "../lib/labels"; | |
| 9 | 9 | import { repoAt } from "../lib/markdown-plugins"; | |
| 10 | 10 | import { Markdown } from "./markdown"; | |
| 11 | + | import { Reactions } from "./reactions"; | |
| 11 | 12 | import { IssueIcon, PullIcon } from "./work-icons"; | |
| 12 | 13 | import { MentionTextarea } from "./mention-textarea"; | |
| 13 | 14 | import { ErrorText, SubmitButton, TimeAgo } from "./ui"; | |
| ⋯ | |||
| 254 | 255 | * wrote. The author's avatar sits beside it, as on any forge. | |
| 255 | 256 | */ | |
| 256 | 257 | export function TimelineItem({ | |
| 258 | + | id, | |
| 257 | 259 | author, | |
| 260 | + | bot = false, | |
| 258 | 261 | agent, | |
| 259 | 262 | action, | |
| 260 | 263 | at, | |
| ⋯ | |||
| 262 | 265 | aside, | |
| 263 | 266 | children, | |
| 264 | 267 | }: { | |
| 268 | + | /** Its anchor, so a link can point at it (`#cmt_…`). */ | |
| 269 | + | id?: string; | |
| 265 | 270 | author: string; | |
| 271 | + | /** g1t, or an agent in someone's harness: drawn as an agent, with no profile to link to. */ | |
| 272 | + | bot?: boolean; | |
| 266 | 273 | /** | |
| 267 | 274 | * Set when one of the workspace's former agents wrote it, as itself: | |
| 268 | 275 | * shown by its name with an Agent badge, and whom it acted for, in place | |
| ⋯ | |||
| 282 | 289 | agent ? ( | |
| 283 | 290 | <Avatar name={agent.ref.displayName} agent size={size} /> | |
| 284 | 291 | ) : ( | |
| 285 | − | <Avatar name={author} size={size} /> | |
| 292 | + | <Avatar name={author} size={size} agent={bot} /> | |
| 286 | 293 | ); | |
| 287 | 294 | return ( | |
| 288 | − | <div className="flex gap-3"> | |
| 295 | + | <div id={id} className="flex scroll-mt-20 gap-3"> | |
| 289 | 296 | <span className="mt-1 hidden shrink-0 sm:block"> | |
| 290 | − | {agent ? ( | |
| 297 | + | {agent || bot ? ( | |
| 291 | 298 | face(32) | |
| 292 | 299 | ) : ( | |
| 293 | 300 | <PersonLink name={author} label={`${author}'s profile`}> | |
| ⋯ | |||
| 301 | 308 | <span className="sm:hidden">{face(18)}</span> | |
| 302 | 309 | {agent ? ( | |
| 303 | 310 | <AgentName agent={agent.ref} /> | |
| 311 | + | ) : bot ? ( | |
| 312 | + | <span className="font-medium text-fg">{author}</span> | |
| 304 | 313 | ) : ( | |
| 305 | 314 | <PersonLink name={author} className="font-medium text-fg hover:underline" /> | |
| 306 | 315 | )} | |
| ⋯ | |||
| 555 | 564 | ); | |
| 556 | 565 | } | |
| 557 | 566 | ||
| 567 | + | /** Something else in a conversation, put among its comments by when it happened: a session, say. */ | |
| 568 | + | export type TimelineExtra = { id: string; at: string; node: ReactNode }; | |
| 569 | + | ||
| 558 | 570 | /** | |
| 559 | 571 | * The conversation of an issue or a pull request, oldest first: what | |
| 560 | − | * people and agents wrote, and between those, what happened. | |
| 572 | + | * people and agents wrote, and between those, what happened, with | |
| 573 | + | * `extra` (its sessions) where each happened. | |
| 561 | 574 | */ | |
| 562 | 575 | export function CommentList({ | |
| 563 | 576 | comments, | |
| 564 | 577 | review, | |
| 565 | 578 | base, | |
| 566 | 579 | viewerId, | |
| 580 | + | viewerName = null, | |
| 567 | 581 | canModerate = false, | |
| 568 | 582 | failed, | |
| 583 | + | extra = [], | |
| 569 | 584 | }: { | |
| 570 | 585 | comments: Comment[]; | |
| 571 | 586 | review?: Review; | |
| ⋯ | |||
| 577 | 592 | canModerate?: boolean; | |
| 578 | 593 | /** Why editing or deleting a comment was refused, under that comment. */ | |
| 579 | 594 | failed?: { comment: string; error?: string | null } | null; | |
| 595 | + | /** The signed-in viewer's username, who may react; none to only show reactions. */ | |
| 596 | + | viewerName?: string | null; | |
| 597 | + | extra?: TimelineExtra[]; | |
| 580 | 598 | }) { | |
| 599 | + | // Each extra goes before the first comment written after it. | |
| 600 | + | const later = [...extra].sort((a, b) => Date.parse(a.at) - Date.parse(b.at)); | |
| 601 | + | const before = (at: string) => { | |
| 602 | + | const due: TimelineExtra[] = []; | |
| 603 | + | while (later.length > 0 && Date.parse(later[0].at) <= Date.parse(at)) due.push(later.shift()!); | |
| 604 | + | return due.map((one) => <Fragment key={one.id}>{one.node}</Fragment>); | |
| 605 | + | }; | |
| 581 | 606 | return ( | |
| 582 | 607 | <> | |
| 583 | − | {comments.map((comment) => { | |
| 608 | + | {comments.map((comment) => ( | |
| 609 | + | <Fragment key={comment.id}> | |
| 610 | + | {before(comment.createdAt)} | |
| 611 | + | {entry(comment)} | |
| 612 | + | </Fragment> | |
| 613 | + | ))} | |
| 614 | + | {later.map((one) => ( | |
| 615 | + | <Fragment key={one.id}>{one.node}</Fragment> | |
| 616 | + | ))} | |
| 617 | + | </> | |
| 618 | + | ); | |
| 619 | + | ||
| 620 | + | function entry(comment: Comment) { | |
| 584 | 621 | if (comment.kind === "event") { | |
| 585 | 622 | return <TimelineEvent key={comment.id} comment={comment} base={base} filesUrl={review?.changesUrl} />; | |
| 586 | 623 | } | |
| ⋯ | |||
| 593 | 630 | return ( | |
| 594 | 631 | <TimelineItem | |
| 595 | 632 | key={comment.id} | |
| 633 | + | id={comment.id} | |
| 596 | 634 | author={comment.author.username} | |
| 597 | 635 | agent={ | |
| 598 | 636 | comment.agent && { | |
| ⋯ | |||
| 626 | 664 | } | |
| 627 | 665 | > | |
| 628 | 666 | {comment.body && <Markdown source={comment.body} repo={repoAt(base)} />} | |
| 667 | + | <Reactions reactions={comment.reactions} commentId={comment.id} viewer={viewerName} className="mt-3" /> | |
| 629 | 668 | {(can.edit || can.delete) && ( | |
| 630 | 669 | <CommentActions comment={comment} can={can} error={failed?.comment === comment.id ? failed.error : null} /> | |
| 631 | 670 | )} | |
| 632 | 671 | </TimelineItem> | |
| 633 | 672 | ); | |
| 634 | − | })} | |
| 635 | − | </> | |
| 636 | − | ); | |
| 673 | + | } | |
| 637 | 674 | } | |
| 638 | 675 | ||
| 639 | 676 | /** Where a signed-in person writes a comment, or a review. */ | |
| 1 | 1 | import { Bot, ExternalLink, GitCommitHorizontal, GitMerge } from "lucide-react"; | |
| 2 | 2 | import { Form, Link, redirect } from "react-router"; | |
| 3 | 3 | ||
| 4 | − | import { AGENT_MODES, type Pull, PROVIDERS, workOwner } from "@g1t/contracts"; | |
| 4 | + | import { AGENT_MODES, type Pull, PROVIDERS, REACTIONS, workOwner } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/issue"; | |
| 7 | 7 | import { excerpt, page } from "../../lib/meta"; | |
| ⋯ | |||
| 39 | 39 | import { DetailsDisclosure, detailsSummary } from "../../components/details-disclosure"; | |
| 40 | 40 | import { repoPages } from "../../lib/repo-live"; | |
| 41 | 41 | import { useRepoLive } from "../../lib/use-repo-live"; | |
| 42 | − | import { DecisionList, IntegrationFailure, IssueWorkPanel, StartSessionDialog } from "../../components/coordination"; | |
| 42 | + | import { DecisionList, IntegrationFailure, IssueWorkPanel, StartSessionDialog, TimelineSession } from "../../components/coordination"; | |
| 43 | + | import { Reactions } from "../../components/reactions"; | |
| 43 | 44 | import { parseBlockedBy, resolutionOf } from "../../lib/coordination"; | |
| 44 | 45 | ||
| 45 | 46 | ||
| ⋯ | |||
| 173 | 174 | }); | |
| 174 | 175 | return result.ok ? null : { error: result.error.message }; | |
| 175 | 176 | } | |
| 177 | + | // A reaction to the issue or one of its comments, or taking one back. | |
| 178 | + | case "react": { | |
| 179 | + | const content = REACTIONS.find((one) => one.content === form.get("content"))?.content; | |
| 180 | + | if (!content) return { error: "Pick a reaction.", action: "react" }; | |
| 181 | + | const result = await work.react(user, path, number, { | |
| 182 | + | commentId: String(form.get("comment") ?? "") || undefined, | |
| 183 | + | content, | |
| 184 | + | remove: form.get("remove") === "1", | |
| 185 | + | }); | |
| 186 | + | return result.ok ? null : { error: result.error.message, action: "react" }; | |
| 187 | + | } | |
| 176 | 188 | case "assign": { | |
| 177 | 189 | const result = await work.updateIssue(user, path, number, { | |
| 178 | 190 | assignees: [ | |
| ⋯ | |||
| 383 | 395 | </Link> | |
| 384 | 396 | )} | |
| 385 | 397 | ||
| 386 | − | {issue.body && ( | |
| 398 | + | {issue.body ? ( | |
| 387 | 399 | <div className={`${issue.resolvedBy != null ? "mt-5" : "lg:mt-5"} rounded-xl border border-line bg-surface p-4 sm:p-5`}> | |
| 388 | 400 | <Markdown source={issue.body} repo={{ namespace: params.owner, name: params.repo }} /> | |
| 401 | + | <Reactions reactions={loaderData.reactions} viewer={viewer?.username ?? null} className="mt-4" /> | |
| 389 | 402 | </div> | |
| 403 | + | ) : ( | |
| 404 | + | (loaderData.reactions?.length ?? 0) > 0 && ( | |
| 405 | + | <Reactions reactions={loaderData.reactions} viewer={viewer?.username ?? null} className="mt-3 lg:mt-5" /> | |
| 406 | + | ) | |
| 390 | 407 | )} | |
| 391 | 408 | ||
| 392 | 409 | <div className="mt-10 flex items-baseline justify-between"> | |
| ⋯ | |||
| 437 | 454 | comments={comments} | |
| 438 | 455 | base={base} | |
| 439 | 456 | viewerId={viewer?.id ?? null} | |
| 457 | + | viewerName={viewer?.username ?? null} | |
| 458 | + | extra={(loaderData.sessionTrees ?? []) | |
| 459 | + | .filter((session) => !session.parentId) | |
| 460 | + | .map((session) => ({ id: session.id, at: session.startedAt, node: <TimelineSession session={session} base={base} /> }))} | |
| 440 | 461 | canModerate={loaderData.canModerate} | |
| 441 | 462 | failed={actionData && "comment" in actionData ? { comment: String(actionData.comment), error: actionData.error } : null} | |
| 442 | 463 | /> | |
| ⋯ | |||
| 463 | 484 | </div> | |
| 464 | 485 | <div className="mt-2"> | |
| 465 | 486 | {/* A refused comment edit says so under the comment. */} | |
| 466 | − | {!(actionData && ("comment" in actionData || ("action" in actionData && (actionData.action === "blocked-by" || actionData.action === "start-session")))) && ( | |
| 487 | + | {!(actionData && ("comment" in actionData || ("action" in actionData && (actionData.action === "blocked-by" || actionData.action === "start-session" || actionData.action === "react")))) && ( | |
| 467 | 488 | <ErrorText>{actionData?.error}</ErrorText> | |
| 468 | 489 | )} | |
| 469 | 490 | </div> | |
| ⋯ | |||
| 530 | 551 | <ul className="mt-2 space-y-1.5 text-sm"> | |
| 531 | 552 | {issue.assignees.map((name) => ( | |
| 532 | 553 | <li key={name} className="flex items-center gap-2 px-1"> | |
| 533 | − | <Avatar name={name} size={20} /> | |
| 554 | + | <Avatar name={name} size={20} agent={name === "g1t"} /> | |
| 534 | 555 | <span className="grow truncate font-mono text-xs"> | |
| 535 | − | <PersonLink name={name} className="hover:underline" /> | |
| 556 | + | {name === "g1t" ? "g1t" : <PersonLink name={name} className="hover:underline" />} | |
| 536 | 557 | </span> | |
| 537 | 558 | </li> | |
| 538 | 559 | ))} | |
| ⋯ | |||
| 557 | 578 | ||
| 558 | 579 | {viewer && can.triage && open && ( | |
| 559 | 580 | <div className="mt-3 space-y-2"> | |
| 581 | + | {/* g1t takes it on in a session on the workspace's Claude, as whoever assigned it. */} | |
| 582 | + | {loaderData.agentSetup && !issue.assignees.includes("g1t") && ( | |
| 583 | + | <Form method="post"> | |
| 584 | + | <input type="hidden" name="action" value="assign" /> | |
| 585 | + | {issue.assignees.map((name) => ( | |
| 586 | + | <input key={name} type="hidden" name="assignee" value={name} /> | |
| 587 | + | ))} | |
| 588 | + | <input type="hidden" name="assignee" value="g1t" /> | |
| 589 | + | <div className="*:w-full"> | |
| 590 | + | <SubmitButton variant="outline" name="who" value="g1t" pending="Assigning…"> | |
| 591 | + | <Bot size={14} /> | |
| 592 | + | Assign to g1t | |
| 593 | + | </SubmitButton> | |
| 594 | + | </div> | |
| 595 | + | </Form> | |
| 596 | + | )} | |
| 560 | 597 | {!issue.assignees.includes(viewer.username) && ( | |
| 561 | 598 | <Form method="post"> | |
| 562 | 599 | <input type="hidden" name="action" value="assign" /> | |
| 40 | 40 | type Viewer, | |
| 41 | 41 | fail, | |
| 42 | 42 | pullComparison, | |
| 43 | + | REACTIONS, | |
| 43 | 44 | workOwner, | |
| 44 | 45 | } from "@g1t/contracts"; | |
| 45 | 46 | ||
| ⋯ | |||
| 69 | 70 | import { CheckboxOption } from "../../components/ui/checkbox"; | |
| 70 | 71 | import { Hint } from "../../components/ui/hint"; | |
| 71 | 72 | import { TabStrip } from "../../components/ui/tab-strip"; | |
| 72 | − | import { DecisionList, IntegrationFailure, SessionTreeList } from "../../components/coordination"; | |
| 73 | + | import { DecisionList, IntegrationFailure, SessionTreeList, TimelineSession } from "../../components/coordination"; | |
| 74 | + | import { Reactions } from "../../components/reactions"; | |
| 73 | 75 | import { resolutionOf, sessionTree, standingFailure } from "../../lib/coordination"; | |
| 74 | 76 | import { WorkflowStatuses } from "../../components/actions"; | |
| 75 | 77 | import { AddCiPrompt } from "../../components/add-ci"; | |
| ⋯ | |||
| 180 | 182 | // Its decision records: what the merge queue found when it was combined | |
| 181 | 183 | // with others, and the reasons kept on it (decisions.rs). | |
| 182 | 184 | tab !== "changes" ? work.listDecisions(path, number, viewer).catch(() => null) : null, | |
| 183 | − | // The harness sessions tied to it in any way, with their subagents, for the Session tab. | |
| 184 | − | tab === "session" ? work.listWorkSessions(path, viewer, { pull: number, subagents: true }).catch(() => null) : null, | |
| 185 | + | // The harness sessions tied to it in any way: with their subagents for | |
| 186 | + | // the Session tab, and in the conversation where each started. | |
| 187 | + | tab === "session" || tab === "conversation" | |
| 188 | + | ? work.listWorkSessions(path, viewer, { pull: number, subagents: tab === "session" }).catch(() => null) | |
| 189 | + | : null, | |
| 185 | 190 | ]); | |
| 186 | 191 | if (!found.ok) { | |
| 187 | 192 | // Issues and pull requests share numbers; this one may be an issue. | |
| ⋯ | |||
| 438 | 443 | ...String(form.get("others") ?? "").split(/[\s,]+/).map((name) => name.replace(/^@/, "")), | |
| 439 | 444 | ]; | |
| 440 | 445 | // Its labels, milestone and base, from the sidebar's menus and the header. | |
| 446 | + | // A reaction to the pull request or one of its comments, or taking one back. | |
| 447 | + | if (action === "react") { | |
| 448 | + | const content = REACTIONS.find((one) => one.content === form.get("content"))?.content; | |
| 449 | + | if (!content) return { error: "Pick a reaction.", action }; | |
| 450 | + | const reacted = await work.react(user, path, number, { | |
| 451 | + | commentId: String(form.get("comment") ?? "") || undefined, | |
| 452 | + | content, | |
| 453 | + | remove: form.get("remove") === "1", | |
| 454 | + | }); | |
| 455 | + | return reacted.ok ? null : { error: reacted.error.message, action }; | |
| 456 | + | } | |
| 441 | 457 | if (action === "labels") { | |
| 442 | 458 | const set = await work.setLabels(user, path, number, form.getAll("label").map(String)); | |
| 443 | 459 | return set.ok ? null : { error: set.error.message, action }; | |
| ⋯ | |||
| 1167 | 1183 | } | |
| 1168 | 1184 | > | |
| 1169 | 1185 | {pull.body ? ( | |
| 1170 | − | <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} /> | |
| 1186 | + | <> | |
| 1187 | + | <Markdown source={pull.body} repo={{ namespace: params.owner, name: params.repo }} /> | |
| 1188 | + | <Reactions reactions={loaderData.reactions} viewer={viewer?.username ?? null} className="mt-3" /> | |
| 1189 | + | </> | |
| 1171 | 1190 | ) : ( | |
| 1172 | 1191 | <p className="text-sm text-muted"> | |
| 1173 | 1192 | {working | |
| ⋯ | |||
| 1201 | 1220 | review={review} | |
| 1202 | 1221 | base={base} | |
| 1203 | 1222 | viewerId={viewer?.id ?? null} | |
| 1223 | + | viewerName={viewer?.username ?? null} | |
| 1224 | + | extra={harnessSessions | |
| 1225 | + | .filter((session) => !session.parentId) | |
| 1226 | + | .map((session) => ({ id: session.id, at: session.startedAt, node: <TimelineSession session={session} base={base} /> }))} | |
| 1204 | 1227 | canModerate={canModerate} | |
| 1205 | 1228 | failed={actionData && "comment" in actionData ? { comment: String(actionData.comment), error: actionData.error } : null} | |
| 1206 | 1229 | /> | |
| 49 | 49 | /// The most characters of an instruction kept. | |
| 50 | 50 | pub const MAX_INSTRUCTION_CHARS: usize = 4_000; | |
| 51 | 51 | ||
| 52 | + | /// Who asked g1t in an issue or a pull request, rather than from a | |
| 53 | + | /// button: by assigning it the issue, or by mentioning `@g1t` in a comment. | |
| 54 | + | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 55 | + | pub struct Asked { | |
| 56 | + | /// Their username. | |
| 57 | + | pub by: String, | |
| 58 | + | /// What their comment said; none for an assignment. | |
| 59 | + | #[serde(default)] | |
| 60 | + | pub comment: Option<String>, | |
| 61 | + | /// Where they asked: the comment, or the issue. | |
| 62 | + | pub url: String, | |
| 63 | + | } | |
| 64 | + | ||
| 52 | 65 | /// `start_agent_session`: work on `issue` (or a new issue from `title` and | |
| 53 | − | /// `body`), as `actor`. Returns `Outcome<AgentSessionStarted>`. | |
| 66 | + | /// `body`), or carry on with pull request `pull`, as `actor`. Returns | |
| 67 | + | /// `Outcome<AgentSessionStarted>`. | |
| 54 | 68 | #[derive(Debug, Serialize, Deserialize)] | |
| 55 | 69 | pub struct StartAgentSessionArgs { | |
| 56 | 70 | pub actor: User, | |
| 57 | 71 | pub repo: RepoPath, | |
| 58 | 72 | #[serde(default)] | |
| 59 | 73 | pub issue: Option<u32>, | |
| 74 | + | /// A pull request to carry on with, on its branch: in place of `issue`. | |
| 75 | + | #[serde(default)] | |
| 76 | + | pub pull: Option<u32>, | |
| 77 | + | /// Set when g1t was asked in the thread (integrations' summon.rs). | |
| 78 | + | #[serde(default)] | |
| 79 | + | pub asked: Option<Asked>, | |
| 60 | 80 | /// Without `issue`: a new issue's title, opened first. | |
| 61 | 81 | #[serde(default)] | |
| 62 | 82 | pub title: Option<String>, | |
| ⋯ | |||
| 78 | 98 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 79 | 99 | #[serde(rename_all = "camelCase")] | |
| 80 | 100 | pub struct AgentSessionStarted { | |
| 81 | − | /// The issue it works on. | |
| 101 | + | /// The issue it works on: for a pull request with none, its number. | |
| 82 | 102 | pub issue: u32, | |
| 103 | + | /// The pull request it carries on with, when it was started on one. | |
| 104 | + | #[serde(default)] | |
| 105 | + | pub pull: Option<u32>, | |
| 83 | 106 | /// The session on g1t, when one was started here. | |
| 84 | 107 | #[serde(default)] | |
| 85 | 108 | pub session: Option<Session>, | |
| 1134 | 1134 | ("add_comment", Scope::IssuesWrite), | |
| 1135 | 1135 | ("edit_comment", Scope::IssuesWrite), | |
| 1136 | 1136 | ("delete_comment", Scope::IssuesWrite), | |
| 1137 | + | ("react", Scope::IssuesWrite), | |
| 1137 | 1138 | ("create_commit_comment", Scope::IssuesWrite), | |
| 1138 | 1139 | ("update_commit_comment", Scope::IssuesWrite), | |
| 1139 | 1140 | ("delete_commit_comment", Scope::IssuesWrite), |
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Loading the diff…
Comments
Sign in to comment.