Skip to content

Commit be4cc7d

Browse files
authored
Add PR review views and pull request listing (#43)
- Add GitHub CLI and server support for listing PRs - Expose pull request list query to the web app - Replace the review screen with table, list, and kanban views
1 parent 4079047 commit be4cc7d

13 files changed

Lines changed: 1060 additions & 255 deletions

File tree

apps/server/src/git/Layers/GitHubCli.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ function normalizeRepositoryCloneUrls(
146146
function decodeGitHubJson<S extends Schema.Top>(
147147
raw: string,
148148
schema: S,
149-
operation: "listOpenPullRequests" | "getPullRequest" | "getRepositoryCloneUrls",
149+
operation:
150+
| "listOpenPullRequests"
151+
| "getPullRequest"
152+
| "getRepositoryCloneUrls"
153+
| "listAllPullRequests",
150154
invalidDetail: string,
151155
): Effect.Effect<S["Type"], GitHubCliError, S["DecodingServices"]> {
152156
return Schema.decodeEffect(Schema.fromJsonString(schema))(raw).pipe(
@@ -203,6 +207,48 @@ const makeGitHubCli = Effect.sync(() => {
203207
),
204208
Effect.map((pullRequests) => pullRequests.map(normalizePullRequestSummary)),
205209
),
210+
listAllPullRequests: (input) =>
211+
execute({
212+
cwd: input.cwd,
213+
args: [
214+
"pr",
215+
"list",
216+
"--state",
217+
input.state ?? "open",
218+
"--limit",
219+
String(input.limit ?? 50),
220+
...(input.label ? ["--label", input.label] : []),
221+
"--json",
222+
"number,title,url,baseRefName,headRefName,state,labels,updatedAt,author",
223+
],
224+
}).pipe(
225+
Effect.map((result) => result.stdout.trim()),
226+
Effect.flatMap((raw) => {
227+
if (raw.length === 0) return Effect.succeed([]);
228+
return Effect.try({
229+
try: () => {
230+
const parsed = JSON.parse(raw) as Array<any>;
231+
return parsed.map((pr: any) => ({
232+
number: pr.number,
233+
title: pr.title,
234+
url: pr.url,
235+
baseRefName: pr.baseRefName,
236+
headRefName: pr.headRefName,
237+
state: pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open",
238+
labels: (pr.labels ?? []).map((l: any) => ({ name: l.name, color: l.color ?? "" })),
239+
updatedAt: pr.updatedAt ?? "",
240+
author: pr.author?.login ?? "",
241+
}));
242+
},
243+
catch: (cause) =>
244+
new GitHubCliError({
245+
operation: "listAllPullRequests",
246+
detail: "GitHub CLI returned invalid PR list JSON.",
247+
cause,
248+
}),
249+
});
250+
}),
251+
),
206252
getPullRequest: (input) =>
207253
execute({
208254
cwd: input.cwd,

apps/server/src/git/Layers/GitManager.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,35 @@ function createGitHubCliWithFakeGh(scenario: FakeGhScenario = {}): {
393393
(result) => JSON.parse(result.stdout) as ReadonlyArray<GitHubPullRequestSummary>,
394394
),
395395
),
396+
listAllPullRequests: (input) =>
397+
execute({
398+
cwd: input.cwd,
399+
args: [
400+
"pr",
401+
"list",
402+
"--state",
403+
input.state ?? "open",
404+
"--limit",
405+
String(input.limit ?? 50),
406+
"--json",
407+
"number,title,url,baseRefName,headRefName,state,labels,updatedAt,author",
408+
],
409+
}).pipe(
410+
Effect.map((result) => {
411+
const parsed = JSON.parse(result.stdout) as Array<any>;
412+
return parsed.map((pr: any) => ({
413+
number: pr.number,
414+
title: pr.title,
415+
url: pr.url,
416+
baseRefName: pr.baseRefName,
417+
headRefName: pr.headRefName,
418+
state: pr.state === "MERGED" ? "merged" : pr.state === "CLOSED" ? "closed" : "open",
419+
labels: (pr.labels ?? []).map((l: any) => ({ name: l.name, color: l.color ?? "" })),
420+
updatedAt: pr.updatedAt ?? "",
421+
author: pr.author?.login ?? "",
422+
}));
423+
}),
424+
),
396425
createPullRequest: (input) =>
397426
execute({
398427
cwd: input.cwd,

apps/server/src/git/Layers/GitManager.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,11 +1273,37 @@ export const makeGitManager = Effect.gen(function* () {
12731273
},
12741274
);
12751275

1276+
const listPullRequests: GitManagerShape["listPullRequests"] = Effect.fnUntraced(
1277+
function* (input) {
1278+
const results = yield* gitHubCli.listAllPullRequests({
1279+
cwd: input.cwd,
1280+
...(input.state !== undefined ? { state: input.state } : {}),
1281+
...(input.label !== undefined ? { label: input.label } : {}),
1282+
...(input.limit !== undefined ? { limit: input.limit } : {}),
1283+
});
1284+
1285+
return {
1286+
pullRequests: results.map((pr) => ({
1287+
number: pr.number,
1288+
title: pr.title,
1289+
url: pr.url,
1290+
baseBranch: pr.baseRefName,
1291+
headBranch: pr.headRefName,
1292+
state: pr.state as "open" | "closed" | "merged",
1293+
labels: pr.labels,
1294+
updatedAt: pr.updatedAt,
1295+
author: pr.author,
1296+
})),
1297+
};
1298+
},
1299+
);
1300+
12761301
return {
12771302
status,
12781303
resolvePullRequest,
12791304
preparePullRequestThread,
12801305
runStackedAction,
1306+
listPullRequests,
12811307
} satisfies GitManagerShape;
12821308
});
12831309

apps/server/src/git/Services/GitHubCli.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ export interface GitHubCliShape {
5151
readonly limit?: number;
5252
}) => Effect.Effect<ReadonlyArray<GitHubPullRequestSummary>, GitHubCliError>;
5353

54+
/**
55+
* List all pull requests for the repo, optionally filtered by state and label.
56+
*/
57+
readonly listAllPullRequests: (input: {
58+
readonly cwd: string;
59+
readonly state?: "open" | "closed" | "merged";
60+
readonly label?: string;
61+
readonly limit?: number;
62+
}) => Effect.Effect<
63+
ReadonlyArray<
64+
GitHubPullRequestSummary & {
65+
labels: Array<{ name: string; color: string }>;
66+
updatedAt: string;
67+
author: string;
68+
}
69+
>,
70+
GitHubCliError
71+
>;
72+
5473
/**
5574
* Resolve a pull request by URL, number, or branch-ish identifier.
5675
*/

apps/server/src/git/Services/GitManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99
import {
1010
GitActionProgressEvent,
11+
GitListPullRequestsInput,
12+
GitListPullRequestsResult,
1113
GitPreparePullRequestThreadInput,
1214
GitPreparePullRequestThreadResult,
1315
GitPullRequestRefInput,
@@ -55,6 +57,13 @@ export interface GitManagerShape {
5557
input: GitPreparePullRequestThreadInput,
5658
) => Effect.Effect<GitPreparePullRequestThreadResult, GitManagerServiceError>;
5759

60+
/**
61+
* List pull requests for the repository, optionally filtered by state/label.
62+
*/
63+
readonly listPullRequests: (
64+
input: GitListPullRequestsInput,
65+
) => Effect.Effect<GitListPullRequestsResult, GitManagerServiceError>;
66+
5867
/**
5968
* Run a stacked Git action (`commit`, `commit_push`, `commit_push_pr`).
6069
* When `featureBranch` is set, creates and checks out a feature branch first.

apps/server/src/wsServer.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1757,6 +1757,7 @@ describe("WebSocket Server", () => {
17571757
resolvePullRequest,
17581758
preparePullRequestThread,
17591759
runStackedAction,
1760+
listPullRequests: vi.fn(() => Effect.succeed({ pullRequests: [] })),
17601761
};
17611762

17621763
server = await createTestServer({ cwd: "/test", gitManager });
@@ -1796,6 +1797,7 @@ describe("WebSocket Server", () => {
17961797
resolvePullRequest: vi.fn(() => Effect.succeed(resolvePullRequestResult)),
17971798
preparePullRequestThread: vi.fn(() => Effect.succeed(preparePullRequestThreadResult)),
17981799
runStackedAction: vi.fn(() => Effect.void as any),
1800+
listPullRequests: vi.fn(() => Effect.succeed({ pullRequests: [] })),
17991801
};
18001802

18011803
server = await createTestServer({ cwd: "/test", gitManager });
@@ -1844,6 +1846,7 @@ describe("WebSocket Server", () => {
18441846
resolvePullRequest: vi.fn(() => Effect.void as any),
18451847
preparePullRequestThread: vi.fn(() => Effect.void as any),
18461848
runStackedAction,
1849+
listPullRequests: vi.fn(() => Effect.succeed({ pullRequests: [] })),
18471850
};
18481851

18491852
server = await createTestServer({ cwd: "/test", gitManager });
@@ -1906,6 +1909,7 @@ describe("WebSocket Server", () => {
19061909
resolvePullRequest: vi.fn(() => Effect.void as any),
19071910
preparePullRequestThread: vi.fn(() => Effect.void as any),
19081911
runStackedAction,
1912+
listPullRequests: vi.fn(() => Effect.succeed({ pullRequests: [] })),
19091913
};
19101914

19111915
server = await createTestServer({ cwd: "/test", gitManager });

apps/server/src/wsServer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,11 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
918918
return yield* gitManager.preparePullRequestThread(body);
919919
}
920920

921+
case WS_METHODS.gitListPullRequests: {
922+
const body = stripRequestTag(request.body);
923+
return yield* gitManager.listPullRequests(body);
924+
}
925+
921926
case WS_METHODS.gitListBranches: {
922927
const body = stripRequestTag(request.body);
923928
return yield* git.listBranches(body);

apps/web/src/lib/gitReactQuery.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const gitQueryKeys = {
1111
all: ["git"] as const,
1212
status: (cwd: string | null) => ["git", "status", cwd] as const,
1313
branches: (cwd: string | null) => ["git", "branches", cwd] as const,
14+
pullRequests: (cwd: string | null) => ["git", "pull-requests", cwd] as const,
1415
};
1516

1617
export const gitMutationKeys = {
@@ -78,6 +79,37 @@ export function gitResolvePullRequestQueryOptions(input: {
7879
});
7980
}
8081

82+
export function gitListPullRequestsQueryOptions(input: {
83+
cwd: string | null;
84+
state?: "open" | "closed" | "merged";
85+
label?: string;
86+
}) {
87+
return queryOptions({
88+
queryKey: [
89+
"git",
90+
"pull-requests",
91+
input.cwd,
92+
input.state ?? "open",
93+
input.label ?? "",
94+
] as const,
95+
queryFn: async () => {
96+
const api = ensureNativeApi();
97+
if (!input.cwd) throw new Error("Pull request listing is unavailable.");
98+
return api.git.listPullRequests({
99+
cwd: input.cwd,
100+
...(input.state ? { state: input.state } : {}),
101+
...(input.label ? { label: input.label } : {}),
102+
limit: 100,
103+
});
104+
},
105+
enabled: input.cwd !== null,
106+
staleTime: 15_000,
107+
refetchOnWindowFocus: true,
108+
refetchOnReconnect: true,
109+
refetchInterval: 30_000,
110+
});
111+
}
112+
81113
export function gitInitMutationOptions(input: { cwd: string | null; queryClient: QueryClient }) {
82114
return mutationOptions({
83115
mutationKey: gitMutationKeys.init(input.cwd),

0 commit comments

Comments
 (0)