Skip to content

Commit 91ad734

Browse files
committed
Show recent PR review activity in the dashboard (#431)
- Add recent maintainer reviews and decision summary to PR review - Surface PR Review in navigation and register Copilot settings
1 parent 9e4670c commit 91ad734

9 files changed

Lines changed: 158 additions & 6 deletions

File tree

apps/server/src/openclaw/GatewayClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ export class OpenclawGatewayClient {
466466

467467
if (frame.type === "event" && typeof frame.event === "string") {
468468
let matchedWaiter = false;
469-
for (const waiter of [...this.pendingEventWaiters]) {
469+
for (const waiter of this.pendingEventWaiters) {
470470
if (waiter.eventName === frame.event) {
471471
matchedWaiter = true;
472472
this.pendingEventWaiters.delete(waiter);

apps/server/src/prReview/Layers/PrReview.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ query PullRequestReviewDashboard($owner: String!, $name: String!, $number: Int!)
6161
headRefName
6262
baseRefOid
6363
headRefOid
64+
reviews(last: 100) {
65+
nodes {
66+
state
67+
body
68+
submittedAt
69+
authorAssociation
70+
author {
71+
login
72+
}
73+
}
74+
}
6475
labels(first: 20) {
6576
nodes { name color }
6677
}
@@ -300,6 +311,32 @@ function normalizeStatusChecks(raw: unknown): PrReviewSummary["statusChecks"] {
300311
return statusChecks;
301312
}
302313

314+
function normalizeRecentReviews(raw: unknown): PrReviewSummary["recentReviews"] {
315+
if (!Array.isArray(raw)) return [];
316+
const maintainerAssociations = new Set(["COLLABORATOR", "MEMBER", "OWNER"]);
317+
return raw
318+
.map((entry) => {
319+
if (!entry || typeof entry !== "object") return null;
320+
const record = entry as Record<string, unknown>;
321+
if (!maintainerAssociations.has(asString(record.authorAssociation) ?? "")) return null;
322+
const submittedAt = asString(record.submittedAt);
323+
const state = asString(record.state);
324+
const author =
325+
record.author && typeof record.author === "object"
326+
? asString((record.author as Record<string, unknown>).login)
327+
: null;
328+
if (!submittedAt || !state || !author) return null;
329+
return {
330+
authorLogin: author,
331+
state,
332+
body: typeof record.body === "string" ? record.body : "",
333+
submittedAt,
334+
} satisfies PrReviewSummary["recentReviews"][number];
335+
})
336+
.filter((entry): entry is PrReviewSummary["recentReviews"][number] => entry !== null)
337+
.toSorted((a, b) => Date.parse(b.submittedAt) - Date.parse(a.submittedAt));
338+
}
339+
303340
function normalizeDashboardResponse(
304341
raw: unknown,
305342
): Pick<PrReviewDashboardResult, "pullRequest" | "threads"> {
@@ -343,6 +380,7 @@ function normalizeDashboardResponse(
343380
((pullRequest.commits as any)?.nodes?.[0] as any)?.commit?.statusCheckRollup?.contexts?.nodes ??
344381
[],
345382
);
383+
const recentReviews = normalizeRecentReviews((pullRequest.reviews as any)?.nodes ?? []);
346384

347385
const threads = Array.isArray((pullRequest.reviewThreads as any)?.nodes)
348386
? ((pullRequest.reviewThreads as any).nodes as unknown[])
@@ -394,6 +432,7 @@ function normalizeDashboardResponse(
394432
.map((entry) => normalizeUser(entry))
395433
.filter((entry): entry is GitHubUserPreview => entry !== null)
396434
.map((user) => ({ user, role: "requestedReviewer" as const })),
435+
recentReviews,
397436
totalThreadCount: threads.length,
398437
unresolvedThreadCount: threads.filter((thread) => !thread.isResolved).length,
399438
headSha: asString(pullRequest.headRefOid),

apps/web/src/appSettings.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ describe("getProviderStartOptions", () => {
7171
claudeAuthTokenHelperCommand: "op read op://shared/anthropic/token --no-newline",
7272
codexBinaryPath: "",
7373
codexHomePath: "",
74+
copilotBinaryPath: "",
75+
copilotConfigDir: "",
7476
openclawGatewayUrl: "",
7577
openclawPassword: "",
7678
}),

apps/web/src/components/CommandPalette.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SunIcon,
1414
GitBranchIcon,
1515
GitMergeIcon,
16+
GitPullRequestIcon,
1617
SearchIcon,
1718
KeyboardIcon,
1819
} from "lucide-react";
@@ -222,6 +223,17 @@ function CommandsView() {
222223
void navigate({ to: "/skills", search: { create: undefined, name: undefined } });
223224
},
224225
});
226+
cmds.push({
227+
id: "nav-pr-review",
228+
label: "Open PR Review",
229+
keywords: ["pr review", "pull request", "review", "github", "maintainer"],
230+
icon: GitPullRequestIcon,
231+
group: "Navigation",
232+
onSelect: () => {
233+
closePalette();
234+
void navigate({ to: "/pr-review" });
235+
},
236+
});
225237

226238
// ── Project quick-switch (inline, first 5) ──
227239
for (const project of projects.slice(0, 5)) {

apps/web/src/components/Sidebar.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
ChevronsDownUpIcon,
3434
ChevronsUpDownIcon,
3535
CircleDotIcon,
36-
ExternalLinkIcon,
3736
FolderIcon,
3837
GitBranchIcon,
3938
GitMergeIcon,
@@ -2290,8 +2289,8 @@ export default function Sidebar() {
22902289
className="gap-2 px-2 py-1.5 text-muted-foreground/70 hover:bg-accent hover:text-foreground"
22912290
onClick={() => void navigate({ to: "/pr-review" })}
22922291
>
2293-
<ExternalLinkIcon className="size-3.5" />
2294-
<span className="text-xs">Open Workspace</span>
2292+
<GitPullRequestIcon className="size-3.5" />
2293+
<span className="text-xs">PR Review</span>
22952294
</SidebarMenuButton>
22962295
</SidebarMenuItem>
22972296
<SidebarMenuItem>

apps/web/src/components/chat/ProviderSetupCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const PROVIDER_CONFIG = {
3535
installCmd: "npm install -g @github/copilot",
3636
authCmd: "copilot login",
3737
verifyCmd: "gh auth status",
38+
note: undefined,
3839
},
3940
} as const;
4041

apps/web/src/components/pr-review/PrReviewShell.tsx

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ function resolvePrReviewConfigPath(projectCwd: string, configPath: string): stri
6161
return joinPath(projectCwd, configPath);
6262
}
6363

64+
function formatReviewDecision(decision: string | null | undefined): string {
65+
if (!decision) return "No decision";
66+
return decision.toLowerCase().replaceAll("_", " ");
67+
}
68+
69+
function reviewDecisionTone(decision: string | null | undefined): string {
70+
switch (decision) {
71+
case "APPROVED":
72+
return "text-emerald-600 dark:text-emerald-400";
73+
case "CHANGES_REQUESTED":
74+
case "REVIEW_REQUIRED":
75+
return "text-amber-600 dark:text-amber-400";
76+
default:
77+
return "text-muted-foreground";
78+
}
79+
}
80+
81+
function formatReviewTimestamp(value: string): string {
82+
const date = new Date(value);
83+
if (Number.isNaN(date.getTime())) return value;
84+
return new Intl.DateTimeFormat(undefined, {
85+
month: "short",
86+
day: "numeric",
87+
hour: "numeric",
88+
minute: "2-digit",
89+
}).format(date);
90+
}
91+
6492
export function PrReviewShell({
6593
project,
6694
projects,
@@ -360,6 +388,8 @@ export function PrReviewShell({
360388
const blockingWorkflowStepsComputed = (dashboardQuery.data?.workflowSteps ?? []).filter(
361389
(step) => step.status === "blocked" || step.status === "failed",
362390
);
391+
const recentReviews = dashboardQuery.data?.pullRequest.recentReviews ?? [];
392+
const displayedRecentReviews = recentReviews.slice(0, 3);
363393

364394
// Inspector props helper
365395
const inspectorProps = {
@@ -531,7 +561,58 @@ export function PrReviewShell({
531561
)}
532562
>
533563
<div className="overflow-hidden">
534-
<div className="px-4 py-3 space-y-3">
564+
<div className="space-y-3 px-4 py-3">
565+
<div className="flex flex-wrap items-start gap-x-4 gap-y-2 rounded-xl border border-border/60 bg-muted/30 px-3 py-2.5 text-xs">
566+
<div className="space-y-0.5">
567+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
568+
Review decision
569+
</div>
570+
<div
571+
className={cn(
572+
"font-medium capitalize",
573+
reviewDecisionTone(dashboardQuery.data?.pullRequest.reviewDecision),
574+
)}
575+
>
576+
{formatReviewDecision(dashboardQuery.data?.pullRequest.reviewDecision)}
577+
</div>
578+
</div>
579+
</div>
580+
<div className="space-y-1.5">
581+
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">
582+
Recent maintainer reviews
583+
</div>
584+
{displayedRecentReviews.length > 0 ? (
585+
<div className="space-y-1.5">
586+
{displayedRecentReviews.map((review) => (
587+
<div
588+
className="rounded-md border border-border/60 bg-muted/30 px-2.5 py-2 text-xs"
589+
key={`${review.authorLogin}:${review.submittedAt}`}
590+
>
591+
<div className="flex items-center justify-between gap-2">
592+
<div className="min-w-0">
593+
<span className="font-medium text-foreground">
594+
{review.authorLogin}
595+
</span>
596+
<span className="ml-2 capitalize text-muted-foreground">
597+
{review.state.toLowerCase().replaceAll("_", " ")}
598+
</span>
599+
</div>
600+
<span className="shrink-0 text-muted-foreground">
601+
{formatReviewTimestamp(review.submittedAt)}
602+
</span>
603+
</div>
604+
{review.body.trim().length > 0 ? (
605+
<p className="mt-1 line-clamp-2 whitespace-pre-wrap text-muted-foreground">
606+
{review.body}
607+
</p>
608+
) : null}
609+
</div>
610+
))}
611+
</div>
612+
) : (
613+
<div className="text-xs text-muted-foreground">No maintainer reviews yet.</div>
614+
)}
615+
</div>
535616
<PrMentionComposer
536617
cwd={project.cwd}
537618
participants={dashboardQuery.data?.pullRequest.participants ?? []}
@@ -540,7 +621,6 @@ export function PrReviewShell({
540621
value={reviewBody}
541622
onChange={(value) => {
542623
setReviewBody(value);
543-
// Auto-expand when user starts typing
544624
if (value.trim().length > 0 && !actionRailExpanded) {
545625
setActionRailExpanded(true);
546626
}

apps/web/src/routes/_chat.settings.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ const PROVIDER_AUTH_GUIDES: Record<
384384
verifyCmd: "Test Connection",
385385
note: "OpenClaw uses the gateway URL and password below rather than a local CLI login. A configured gateway unlocks it for new-thread selection.",
386386
},
387+
copilot: {
388+
installCmd: "npm install -g @github/copilot",
389+
authCmd: "copilot login",
390+
verifyCmd: "gh auth status",
391+
note: "GitHub Copilot must be installed and authenticated before it can appear in the thread picker.",
392+
},
387393
};
388394

389395
function getAuthenticationBadgeCopy(input: {
@@ -811,6 +817,7 @@ function SettingsRouteView() {
811817
codex: Boolean(settings.codexBinaryPath || settings.codexHomePath),
812818
claudeAgent: Boolean(settings.claudeBinaryPath || settings.claudeAuthTokenHelperCommand),
813819
openclaw: Boolean(settings.openclawGatewayUrl || settings.openclawPassword),
820+
copilot: Boolean(settings.copilotBinaryPath || settings.copilotConfigDir),
814821
});
815822
const [selectedCustomModelProvider, setSelectedCustomModelProvider] =
816823
useState<ProviderKind>("codex");
@@ -820,6 +827,7 @@ function SettingsRouteView() {
820827
codex: "",
821828
claudeAgent: "",
822829
openclaw: "",
830+
copilot: "",
823831
});
824832
const [customModelErrorByProvider, setCustomModelErrorByProvider] = useState<
825833
Partial<Record<ProviderKind, string | null>>
@@ -1187,12 +1195,14 @@ function SettingsRouteView() {
11871195
codex: false,
11881196
claudeAgent: false,
11891197
openclaw: false,
1198+
copilot: false,
11901199
});
11911200
setSelectedCustomModelProvider("codex");
11921201
setCustomModelInputByProvider({
11931202
codex: "",
11941203
claudeAgent: "",
11951204
openclaw: "",
1205+
copilot: "",
11961206
});
11971207
setCustomModelErrorByProvider({});
11981208

@@ -2515,6 +2525,7 @@ function SettingsRouteView() {
25152525
codex: false,
25162526
claudeAgent: false,
25172527
openclaw: false,
2528+
copilot: false,
25182529
});
25192530
}}
25202531
/>

packages/contracts/src/prReview.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ export const PrReviewSummary = Schema.Struct({
227227
statusChecks: Schema.Array(PrReviewStatusCheck),
228228
participants: Schema.Array(PrReviewParticipant),
229229
reviewRequests: Schema.Array(PrReviewParticipant),
230+
recentReviews: Schema.Array(
231+
Schema.Struct({
232+
authorLogin: TrimmedNonEmptyString,
233+
state: TrimmedNonEmptyString,
234+
body: Schema.String,
235+
submittedAt: Schema.String,
236+
}),
237+
),
230238
totalThreadCount: NonNegativeInt,
231239
unresolvedThreadCount: NonNegativeInt,
232240
headSha: Schema.NullOr(TrimmedNonEmptyString),

0 commit comments

Comments
 (0)