Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
51a0d05
feat(web): use the linked pull request row layout on the pull request…
maria-rcks Sep 19, 2026
e090c36
fix(web): let pull request row meta shrink and keep the panel's autho…
maria-rcks Sep 19, 2026
771b10e
test(web): walk only element arrays in the pull request row helper
maria-rcks Sep 19, 2026
7c90434
feat(web): give pull request page rows a little of their old room
maria-rcks Sep 19, 2026
0cf5175
fix(web): size skipped pull request rows and ghosts like rendered rows
maria-rcks Sep 19, 2026
d4a6011
feat(web): drop branch names from pull request page rows
maria-rcks Sep 19, 2026
0ebe401
docs(web): say which pull request list shows branches in the shared row
maria-rcks Sep 19, 2026
da8eb70
feat(web): give pull request page rows a little more room
maria-rcks Sep 20, 2026
303b092
feat(web): show pull request labels as flat tinted tags
maria-rcks Sep 20, 2026
8922ac1
merge: origin/main into maria/pr-list-shared-row
maria-rcks Sep 20, 2026
b8d465c
fix(web): keep label chips readable, shrinkable and one height
maria-rcks Sep 20, 2026
952dc85
fix(web): lean label chip text further to the foreground on light
maria-rcks Sep 20, 2026
4780f43
fix(web): mix label chip text toward the contrast-aware foreground
maria-rcks Sep 20, 2026
6d09bb8
fix(web): darken label chip text a little more on dark
maria-rcks Sep 20, 2026
741f851
feat(web): put checks and the review verdict right after the title
maria-rcks Sep 20, 2026
726f520
fix(web): drop the unused approval glyph and keep the review fallback…
maria-rcks Sep 20, 2026
3e28ed9
feat(web): give pull request groups the sidebar's section header
maria-rcks Sep 21, 2026
7e511a6
fix(web): draw the group header rule with the shared separator
maria-rcks Sep 21, 2026
e22d1ec
fix(web): show the linked pull request row menu on hover without rese…
maria-rcks Sep 21, 2026
a053747
feat(web): pill label chips with a softer wash, review glyph only on …
maria-rcks Sep 21, 2026
31f2cff
feat(server): count a review bot's approval as the pull request's ver…
maria-rcks Sep 21, 2026
1b8699c
feat(web): put the out-of-date warning on the base branch name
maria-rcks Sep 21, 2026
eba7442
fix(web): keep the hidden row menu overlay out of the way of taps
maria-rcks Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/server/src/pullRequest/gitHubPullRequestJson.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ describe("pull request list decoding", () => {
]);
});

it("takes the verdict from the latest reviews when GitHub summarizes none, as for a bot's approval", () => {
const batch = expectSuccess(
decodePullRequestListJson(
listJson([
{
reviewDecision: null,
latestReviews: [{ author: { login: "macroscopeapp" }, state: "APPROVED" }],
},
{
reviewDecision: "REVIEW_REQUIRED",
latestReviews: [
{ author: { login: "octocat" }, state: "APPROVED" },
{ author: { login: "hubot" }, state: "CHANGES_REQUESTED" },
],
},
{
reviewDecision: "APPROVED",
latestReviews: [{ author: { login: "hubot" }, state: "CHANGES_REQUESTED" }],
},
{
reviewDecision: null,
latestReviews: [{ author: { login: "octocat" }, state: "COMMENTED" }],
},
]),
),
);
expect(batch.items.map((entry) => entry.reviewDecision)).toEqual([
"approved",
"changes-requested",
"approved",
null,
]);
});

it("rolls the head commit's checks up to the one word a row has space for", () => {
const batch = expectSuccess(
decodePullRequestListJson(
Expand Down
57 changes: 47 additions & 10 deletions apps/server/src/pullRequest/gitHubPullRequestJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ const RawReviewRequestSchema = Schema.Struct({
name: Schema.optional(Schema.NullOr(Schema.String)),
});

/** One reviewer's most recent review: the state is all the verdict needs, the author is for who. */
const RawLatestReviewSchema = Schema.Struct({
author: Schema.optional(Schema.NullOr(RawActorSchema)),
state: Schema.optional(Schema.NullOr(Schema.String)),
});

const RawCheckSchema = Schema.Struct({
__typename: Schema.optional(Schema.String),
name: Schema.optional(Schema.NullOr(Schema.String)),
Expand Down Expand Up @@ -106,6 +112,7 @@ const RawListItemSchema = Schema.Struct({
updatedAt: Schema.String,
mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
reviewRequests: Schema.optional(Schema.Array(RawReviewRequestSchema)),
latestReviews: Schema.optional(Schema.NullOr(Schema.Array(RawLatestReviewSchema))),
labels: Schema.optional(Schema.Array(RawLabelSchema)),
/**
* Every check of the head commit, which is the only rollup `gh pr list --json` can give: there
Expand Down Expand Up @@ -143,6 +150,9 @@ const RawSearchItemSchema = Schema.Struct({
isDraft: Schema.optional(Schema.Boolean),
mergeable: Schema.optional(Schema.NullOr(Schema.String)),
reviewDecision: Schema.optional(Schema.NullOr(Schema.String)),
latestReviews: Schema.optional(
Schema.NullOr(Schema.Struct({ nodes: Schema.Array(Schema.NullOr(RawLatestReviewSchema)) })),
),
createdAt: Schema.String,
updatedAt: Schema.String,
mergedAt: Schema.optional(Schema.NullOr(Schema.String)),
Expand Down Expand Up @@ -514,13 +524,7 @@ const RawReviewThreadsSchema = Schema.Struct({
),
),
latestReviews: Schema.optional(
Schema.NullOr(
Schema.Struct({
nodes: Schema.Array(
Schema.Struct({ author: Schema.optional(Schema.NullOr(RawActorSchema)) }),
),
}),
),
Schema.NullOr(Schema.Struct({ nodes: Schema.Array(RawLatestReviewSchema) })),
),
reviewDismissals: Schema.optional(
Schema.NullOr(
Expand Down Expand Up @@ -698,7 +702,7 @@ export function decodeActorAvatarsJson(
}

export const PULL_REQUEST_LIST_JSON_FIELDS =
"number,title,url,author,headRefName,baseRefName,state,isDraft,mergeable,reviewDecision,additions,deletions,createdAt,updatedAt,mergedAt,reviewRequests,labels,statusCheckRollup";
"number,title,url,author,headRefName,baseRefName,state,isDraft,mergeable,reviewDecision,additions,deletions,createdAt,updatedAt,mergedAt,reviewRequests,latestReviews,labels,statusCheckRollup";

export const PULL_REQUEST_DETAIL_JSON_FIELDS = `${PULL_REQUEST_LIST_JSON_FIELDS},body,changedFiles,closedAt,isCrossRepository,headRepositoryOwner,headRefOid,autoMergeRequest`;

Expand Down Expand Up @@ -817,6 +821,7 @@ export function pullRequestSearchGraphQlQuery(rows: number, includeStacks = fals
isDraft
mergeable
reviewDecision
latestReviews(first: 20) { nodes { state author { login } } }
createdAt
updatedAt
mergedAt
Expand Down Expand Up @@ -891,7 +896,7 @@ export const REVIEW_THREADS_GRAPHQL_QUERY = `query($owner: String!, $name: Strin
}
}
latestReviews(first: 50) {
nodes { author { __typename login avatarUrl } }
nodes { state author { __typename login avatarUrl } }
}
reviewDismissals: timelineItems(itemTypes: [REVIEW_DISMISSED_EVENT], first: ${GRAPHQL_PAGE_SIZE}) {
pageInfo { hasNextPage endCursor }
Expand Down Expand Up @@ -1325,6 +1330,35 @@ function toMergeMethod(value: string | null | undefined): PullRequestMergeMethod
}
}

/**
* GitHub's own `reviewDecision` counts only reviews that satisfy the branch rules, so an
* approval from an app (a review bot) or from anyone without the required permission leaves it
* empty. The reviewers still said something, and a row should show it: when GitHub reports no
* verdict, the latest review per reviewer decides, changes requested outranking approval.
*/
function toReviewDecisionWithReviews(
value: string | null | undefined,
// `gh pr list` hands the reviews as an array; the GraphQL reads hand a connection.
latestReviews:
| ReadonlyArray<Schema.Schema.Type<typeof RawLatestReviewSchema>>
| { readonly nodes: ReadonlyArray<Schema.Schema.Type<typeof RawLatestReviewSchema>> }
| null
| undefined,
): PullRequestReviewDecision | null {
const summarized = toReviewDecision(value);
if (summarized === "approved" || summarized === "changes-requested") return summarized;
const reviews =
latestReviews === null || latestReviews === undefined
? []
: "nodes" in latestReviews
? latestReviews.nodes
: latestReviews;
const states = new Set(reviews.map((review) => review.state?.trim().toUpperCase() ?? ""));
if (states.has("CHANGES_REQUESTED")) return "changes-requested";
if (states.has("APPROVED")) return "approved";
return summarized;
}

function toReviewDecision(value: string | null | undefined): PullRequestReviewDecision | null {
switch (value?.trim().toUpperCase()) {
case "APPROVED":
Expand Down Expand Up @@ -1558,7 +1592,7 @@ function toListItem(raw: Schema.Schema.Type<typeof RawListItemSchema>): GitHubPu
state: toState(raw),
isDraft: raw.isDraft ?? false,
mergeability: toMergeability(raw.mergeable),
reviewDecision: toReviewDecision(raw.reviewDecision),
reviewDecision: toReviewDecisionWithReviews(raw.reviewDecision, raw.latestReviews),
additions: raw.additions ?? 0,
deletions: raw.deletions ?? 0,
createdAt: raw.createdAt,
Expand Down Expand Up @@ -1681,6 +1715,9 @@ export function decodePullRequestSearchJson(
items.push({
...toListItem({
...node,
latestReviews: (node.latestReviews?.nodes ?? []).flatMap((review) =>
review === null ? [] : [review],
),
reviewRequests: (node.reviewRequests?.nodes ?? []).flatMap((request) => {
const login = trimmed(request?.requestedReviewer?.login);
return login === null ? [] : [{ login }];
Expand Down
128 changes: 82 additions & 46 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
useMemo,
useRef,
useState,
type ReactNode,
} from "react";

import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
Expand Down Expand Up @@ -344,6 +345,8 @@ function PullRequestBaseFreshnessWarning({
pending,
onUpdate,
iconClassName,
className,
children,
}: {
readonly baseBranch: string;
readonly freshness: {
Expand All @@ -353,6 +356,9 @@ function PullRequestBaseFreshnessWarning({
readonly pending: boolean;
readonly onUpdate: (method: PullRequestUpdateMethod) => void;
readonly iconClassName?: string;
readonly className?: string;
/** What the warning is about, drawn in the same amber before the mark: the base branch. */
readonly children?: ReactNode;
}) {
const behind =
freshness.behindBy === null
Expand All @@ -371,11 +377,15 @@ function PullRequestBaseFreshnessWarning({
<button
type="button"
aria-label={summary}
className="inline-flex shrink-0 cursor-help items-center rounded-sm text-amber-600 outline-none focus-visible:ring-2 focus-visible:ring-ring"
className={cn(
"inline-flex min-w-0 shrink-0 cursor-help items-center gap-1 rounded-sm text-amber-600 outline-none focus-visible:ring-2 focus-visible:ring-ring dark:text-amber-400/90",
className,
)}
/>
}
>
<TriangleAlertIcon aria-hidden className={cn("size-3.5", iconClassName)} />
{children}
<TriangleAlertIcon aria-hidden className={cn("size-3.5 shrink-0", iconClassName)} />
</PopoverTrigger>
<PopoverPopup
align="start"
Expand Down Expand Up @@ -2239,35 +2249,48 @@ export function PullRequestDetailPanel({
</span>
<span aria-hidden className="h-3 w-px shrink-0 bg-border/70" />
<span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-[11px] text-muted-foreground/65">
<Tooltip>
<TooltipTrigger
render={
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</span>
}
/>
<TooltipPopup side="top">
{isStackedPullRequest
? `Stacked on ${detail.baseBranch}`
: detail.baseBranch}
</TooltipPopup>
</Tooltip>
{/* An out-of-date base wears the warning on the branch name itself, so the
name is amber and pointing at either the name or the mark opens the way
out. Up to date, the name keeps its plain tooltip. */}
{freshness ? (
<PullRequestBaseFreshnessWarning
baseBranch={detail.baseBranch}
freshness={freshness}
pending={actionPending}
onUpdate={(method) => void perform("update-branch", undefined, method)}
iconClassName="size-3"
/>
) : null}
className="max-w-[40%]"
>
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</PullRequestBaseFreshnessWarning>
) : (
<Tooltip>
<TooltipTrigger
render={
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</span>
}
/>
<TooltipPopup side="top">
{isStackedPullRequest
? `Stacked on ${detail.baseBranch}`
: detail.baseBranch}
</TooltipPopup>
</Tooltip>
)}
<ArrowLeftIcon
aria-label="receives changes from"
className="size-3 shrink-0 opacity-60"
Expand Down Expand Up @@ -2412,34 +2435,47 @@ export function PullRequestDetailPanel({

<div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
<span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
<Tooltip>
<TooltipTrigger
render={
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</span>
}
/>
<TooltipPopup side="top">
{isStackedPullRequest
? `Stacked on ${detail.baseBranch}`
: detail.baseBranch}
</TooltipPopup>
</Tooltip>
{/* An out-of-date base wears the warning on the branch name itself, so the
name is amber and pointing at either the name or the mark opens the way
out. Up to date, the name keeps its plain tooltip. */}
{freshness ? (
<PullRequestBaseFreshnessWarning
baseBranch={detail.baseBranch}
freshness={freshness}
pending={actionPending}
onUpdate={(method) => void perform("update-branch", undefined, method)}
/>
) : null}
className="max-w-[40%]"
>
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</PullRequestBaseFreshnessWarning>
) : (
<Tooltip>
<TooltipTrigger
render={
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
{isStackedPullRequest ? (
<PullRequestGlyph.stack
aria-label="Stacked pull request"
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
</span>
}
/>
<TooltipPopup side="top">
{isStackedPullRequest
? `Stacked on ${detail.baseBranch}`
: detail.baseBranch}
</TooltipPopup>
</Tooltip>
)}
<ArrowLeftIcon
aria-label="receives changes from"
className="size-3.5 shrink-0 opacity-60"
Expand Down
Loading
Loading