From 9f4f003eaa1d3622daef1b7b70cdd9a361f39260 Mon Sep 17 00:00:00 2001 From: omartelo Date: Fri, 7 Aug 2026 00:34:45 -0300 Subject: [PATCH 1/2] feat: edit the description and pick reviewers from Overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Overview tab rendered the pull request's body and nothing else, so two ordinary things about a pull request still had to be done on github.com: fixing a description an agent drafted, and choosing who reviews it. Leaving the app mid-review is the one gap left in a screen that already merges, comments and files a review. Overview now carries both. The description takes an Edit that opens the body in place and saves it through `gh pr edit --body`. Beside it sits the review roster, which is two gh fields read as one: reviewRequests is who still owes an answer, latestReviews is who gave one, and GitHub moves a person between them — submitting a review clears the request, asking again re-files it. Either half alone lies, so PRDetail carries them merged, and a reviewer who already answered comes back unticked because ticking them is the re-request. The picker offers GitHub's assignableUsers rather than a typed login: it is what gh's own prompt uses, a read-only account can still see it, and every name in it is one GitHub will accept. The pull request's author is filtered out — GitHub refuses a request addressed to them — and a merged or closed pull request keeps the roster but loses the picker. A team shows on the roster and stays out of the picker: gh addresses one as org/slug, which is not the slug the roster carries. --- CHANGELOG.md | 14 ++ .../src/components/pulls/PullRequestView.tsx | 10 +- .../src/components/pulls/PullsOverview.tsx | 234 ++++++++++++++++++ frontend/src/lib/api-types.ts | 24 ++ frontend/src/lib/pulls/merge-gate.test.ts | 2 + frontend/src/lib/pulls/reviewers.test.ts | 43 ++++ frontend/src/lib/pulls/reviewers.ts | 22 ++ frontend/src/lib/rpc.ts | 11 + internal/project/pr.go | 71 ++++-- internal/project/prreviewers.go | 133 ++++++++++ internal/project/prreviewers_test.go | 194 +++++++++++++++ 11 files changed, 726 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/pulls/PullsOverview.tsx create mode 100644 frontend/src/lib/pulls/reviewers.test.ts create mode 100644 frontend/src/lib/pulls/reviewers.ts create mode 100644 internal/project/prreviewers.go create mode 100644 internal/project/prreviewers_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7815ad7c7..c3bedf952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **The pull request's description is editable in the app, and its reviewers are + picked there too.** The Overview tab was a read-only rendering of the body: a + description an agent wrote and you wanted to trim, and choosing who reviews, + were the last two things about a pull request that sent you to github.com in + the middle of the work. Overview now says who opened it, lists the review + roster — who was asked, who approved, who requested changes — with **Request + review** beside it, ticking anyone the repository allows on a review, and + carries an **Edit** for the description that saves straight to GitHub. A + reviewer who already answered comes back unticked: ticking them again is the + re-request. Merged and closed pull requests keep the roster and lose the + picker, which GitHub would refuse anyway. + ### Fixed - **Windows: the app list and the file picker wear the current icon.** The icon diff --git a/frontend/src/components/pulls/PullRequestView.tsx b/frontend/src/components/pulls/PullRequestView.tsx index 9f07b8d77..a95053e93 100644 --- a/frontend/src/components/pulls/PullRequestView.tsx +++ b/frontend/src/components/pulls/PullRequestView.tsx @@ -33,7 +33,6 @@ import { } from "@/lib/pulls/merge-gate" import { useBranchRules } from "@/lib/pulls/use-branch-rules" import { cn, errorText } from "@/lib/utils" -import { Markdown } from "@/components/Markdown" import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -47,6 +46,7 @@ import { PullsChecks } from "./PullsChecks" import { PullsCommits } from "./PullsCommits" import { PullsConversation } from "./PullsConversation" import { PullsFiles } from "./PullsFiles" +import { PullsOverview } from "./PullsOverview" import { ChecksStat, MergeableStat, ReviewStat, StateStat } from "./PullsStats" import type { ThreadActions } from "./ReviewThread" import { SubmitReviewDialog } from "./SubmitReviewDialog" @@ -442,13 +442,7 @@ export function PullRequestView({ /> )} {tab === "overview" && ( -
- {detail.body.trim() !== "" ? ( - {detail.body} - ) : ( -

No description.

- )} -
+ )} )} diff --git a/frontend/src/components/pulls/PullsOverview.tsx b/frontend/src/components/pulls/PullsOverview.tsx new file mode 100644 index 000000000..7e24ba05b --- /dev/null +++ b/frontend/src/components/pulls/PullsOverview.tsx @@ -0,0 +1,234 @@ +import { useState } from "react" +import { toast } from "sonner" +import { + Check, + ChevronDown, + CircleDashed, + MessageSquare, + Pencil, + UserPlus, + Users, + X, + type LucideIcon, +} from "lucide-react" +import type { PullRequestDetail, PullRequestReviewer, ReviewCandidate } from "@/lib/api-types" +import { pendingReviewerLogins, pickableReviewers } from "@/lib/pulls/reviewers" +import { ProjectService } from "@/lib/rpc" +import { cn, errorText } from "@/lib/utils" +import { Markdown } from "@/components/Markdown" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { commentFieldClass } from "./CommentBox" + +interface PullsOverviewProps { + path: string + detail: PullRequestDetail + /** Re-read the pull request: an edit here lands on GitHub, and GitHub's + * answer — not a local copy of it — is what the screen then shows. */ + onRefresh: () => void +} + +// PullsOverview is the "Overview" tab: who opened the pull request, who is +// reviewing it, and what it says. The description and the reviewers are both +// editable here — they were the last two things about a pull request that sent +// the reader out to github.com. +export function PullsOverview({ path, detail, onRefresh }: PullsOverviewProps) { + // null is "not editing"; an empty string is a description being cleared, which + // is a legitimate edit and must not read as the same thing. + const [draft, setDraft] = useState(null) + const [saving, setSaving] = useState(false) + + const save = async () => { + if (draft === null) { + return + } + setSaving(true) + try { + await ProjectService.EditPullRequestBody(path, detail.number, draft) + setDraft(null) + onRefresh() + } catch (err: unknown) { + toast.error(`Couldn’t save the description: ${errorText(err)}`) + } finally { + setSaving(false) + } + } + + return ( +
+
+ {detail.author !== "" && ( + + Opened by {detail.author} + + )} + +
+ +
+
+ Description + {draft === null && ( + + )} +
+ {draft === null ? ( + detail.body.trim() !== "" ? ( + {detail.body} + ) : ( +

No description.

+ ) + ) : ( +
+