);
}
/**
* A verdict, as its own row rather than a line inside a collapsed conversation. It wears the
* reviewer's face on the rail and the verdict's own icon beside their name, so "approved" reads
* at a glance from the same place a merge or a commit does.
*/
function ReviewVerdictEvent({
event,
outcome,
stale,
cwd,
onOpen,
reactions,
}: {
event: PullRequestTimelineEvent;
outcome: PullRequestReviewOutcome;
/** Commits landed after this verdict, so it speaks for code the branch no longer has. */
stale: boolean;
cwd: string;
onOpen: (url: string) => void;
reactions: ReactionSurface;
}) {
return (
{/* Pinned rather than centred: this row grows with a body and a reaction bar, and a
centred avatar drifts down beside them instead of sitting by the name. */}
}
/>
{/* The word alone, in the verdict's own colour — green for an approval, red for a
request for changes. A verdict overtaken by later commits keeps its word and
loses that colour: it still happened, and it no longer speaks for what is on the
branch. Lowercased in the styling rather than the string, so what a screen reader
announces stays the label every other surface uses. */}
}
>
{pullRequestReviewOutcomeLabel(outcome)}
{stale ? , before the latest commits : null}
{pullRequestReviewOutcomeStaleLabel(outcome)}
{/* The reaction bar rides this line rather than taking one of its own. Its add button
is invisible until hovered but still occupies `h-6`, and under a verdict — usually a
single line with no body — a row of that reserved on its own reads as a hole. */}
{/* An approval usually carries no words. When it does they are the review, so they stay
visible rather than being folded away with the ordinary conversation. */}
{event.body ? (
) : null}
);
}
export function PullRequestTimelineTab({
detail,
environmentId,
threadRef = null,
reference,
order,
onOpenCommit,
onRefresh,
}: {
detail: PullRequestDetailView;
environmentId: EnvironmentId;
threadRef?: ScopedThreadRef | null;
reference: PullRequestRef;
order: "newest" | "oldest";
onOpenCommit: (oid: string) => void;
onRefresh: () => void;
}) {
const events = buildPullRequestTimeline(detail);
const newestCommitAt = newestPullRequestCommitAt(detail.commits);
const reactions: ReactionSurface = {
canReact: detail.capabilities.reactions === true,
environmentId,
threadRef,
reference,
onRefresh,
};
// A timeline entry keeps only what it draws, so the remarks this reader may rewrite are looked
// up here by the id the entry carries.
const editable = new Map(
detail.comments
.filter((comment) => canEditPullRequestComment(detail, comment))
.map((comment) => [comment.id, comment] as const),
);
const orderedEvents = order === "newest" ? events : events.toReversed();
const rows = groupPullRequestTimelineConversations(orderedEvents);
const openOnHost = (url: string) => {
void readLocalApi()?.shell.openExternal(url);
};
return (