Skip to content

Commit 6912bf0

Browse files
committed
Improve scrolling and comment behavior
1 parent 3c2f4c1 commit 6912bf0

File tree

10 files changed

+3238
-1018
lines changed

10 files changed

+3238
-1018
lines changed

bun.lock

Lines changed: 68 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"license": "MIT",
2525
"dependencies": {
2626
"@radix-ui/react-collapsible": "^1.1.12",
27+
"@radix-ui/react-context-menu": "^2.2.16",
2728
"@radix-ui/react-slot": "^1.2.4",
29+
"@radix-ui/react-tooltip": "^1.2.8",
2830
"clsx": "^2.1.1",
2931
"diff": "^8.0.2",
3032
"gitdiff-parser": "^0.3.1",

src/api/api.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import {
77
replyToComment,
88
getReviews,
99
createReview,
10+
createPendingReview,
11+
submitPendingReview,
12+
deletePendingReview,
13+
getReviewComments,
14+
updateReviewComment,
15+
deleteReviewComment,
1016
getCheckRuns,
1117
getCombinedStatus,
1218
mergePullRequest,
1319
getIssueComments,
1420
createIssueComment,
21+
getFileContent,
1522
} from "./github";
1623
import { parseDiffWithHighlighting } from "./diff";
1724

@@ -146,7 +153,7 @@ api.get("/pr/:owner/:repo/:number/reviews", async (c) => {
146153
}
147154
});
148155

149-
// Submit a review
156+
// Submit a review (with all comments at once)
150157
api.post("/pr/:owner/:repo/:number/reviews", async (c) => {
151158
const { owner, repo, number } = c.req.param();
152159
const body = await c.req.json();
@@ -170,6 +177,130 @@ api.post("/pr/:owner/:repo/:number/reviews", async (c) => {
170177
}
171178
});
172179

180+
// Create a pending review
181+
api.post("/pr/:owner/:repo/:number/reviews/pending", async (c) => {
182+
const { owner, repo, number } = c.req.param();
183+
const body = await c.req.json();
184+
185+
try {
186+
const review = await createPendingReview(
187+
owner,
188+
repo,
189+
parseInt(number, 10),
190+
body.commit_id
191+
);
192+
return c.json(review);
193+
} catch (error) {
194+
return c.json(
195+
{ error: error instanceof Error ? error.message : "Failed to create pending review" },
196+
500
197+
);
198+
}
199+
});
200+
201+
// Submit a pending review
202+
api.post("/pr/:owner/:repo/:number/reviews/:reviewId/submit", async (c) => {
203+
const { owner, repo, number, reviewId } = c.req.param();
204+
const body = await c.req.json();
205+
206+
try {
207+
const review = await submitPendingReview(
208+
owner,
209+
repo,
210+
parseInt(number, 10),
211+
parseInt(reviewId, 10),
212+
body.event,
213+
body.body
214+
);
215+
return c.json(review);
216+
} catch (error) {
217+
return c.json(
218+
{ error: error instanceof Error ? error.message : "Failed to submit review" },
219+
500
220+
);
221+
}
222+
});
223+
224+
// Delete a pending review
225+
api.delete("/pr/:owner/:repo/:number/reviews/:reviewId", async (c) => {
226+
const { owner, repo, number, reviewId } = c.req.param();
227+
228+
try {
229+
await deletePendingReview(
230+
owner,
231+
repo,
232+
parseInt(number, 10),
233+
parseInt(reviewId, 10)
234+
);
235+
return c.json({ success: true });
236+
} catch (error) {
237+
return c.json(
238+
{ error: error instanceof Error ? error.message : "Failed to delete review" },
239+
500
240+
);
241+
}
242+
});
243+
244+
// Get comments for a specific review
245+
api.get("/pr/:owner/:repo/:number/reviews/:reviewId/comments", async (c) => {
246+
const { owner, repo, number, reviewId } = c.req.param();
247+
248+
try {
249+
const comments = await getReviewComments(
250+
owner,
251+
repo,
252+
parseInt(number, 10),
253+
parseInt(reviewId, 10)
254+
);
255+
return c.json(comments);
256+
} catch (error) {
257+
return c.json(
258+
{ error: error instanceof Error ? error.message : "Failed to fetch review comments" },
259+
500
260+
);
261+
}
262+
});
263+
264+
// Update a review comment
265+
api.patch("/pr/:owner/:repo/comments/:commentId", async (c) => {
266+
const { owner, repo, commentId } = c.req.param();
267+
const body = await c.req.json();
268+
269+
try {
270+
const comment = await updateReviewComment(
271+
owner,
272+
repo,
273+
parseInt(commentId, 10),
274+
body.body
275+
);
276+
return c.json(comment);
277+
} catch (error) {
278+
return c.json(
279+
{ error: error instanceof Error ? error.message : "Failed to update comment" },
280+
500
281+
);
282+
}
283+
});
284+
285+
// Delete a review comment
286+
api.delete("/pr/:owner/:repo/comments/:commentId", async (c) => {
287+
const { owner, repo, commentId } = c.req.param();
288+
289+
try {
290+
await deleteReviewComment(
291+
owner,
292+
repo,
293+
parseInt(commentId, 10)
294+
);
295+
return c.json({ success: true });
296+
} catch (error) {
297+
return c.json(
298+
{ error: error instanceof Error ? error.message : "Failed to delete comment" },
299+
500
300+
);
301+
}
302+
});
303+
173304
// ============================================================================
174305
// Checks & Status
175306
// ============================================================================
@@ -255,4 +386,28 @@ api.post("/pr/:owner/:repo/:number/conversation", async (c) => {
255386
}
256387
});
257388

389+
// ============================================================================
390+
// File Content
391+
// ============================================================================
392+
393+
api.get("/file/:owner/:repo", async (c) => {
394+
const { owner, repo } = c.req.param();
395+
const path = c.req.query("path");
396+
const ref = c.req.query("ref");
397+
398+
if (!path || !ref) {
399+
return c.json({ error: "Missing path or ref query parameter" }, 400);
400+
}
401+
402+
try {
403+
const content = await getFileContent(owner, repo, path, ref);
404+
return c.text(content);
405+
} catch (error) {
406+
return c.json(
407+
{ error: error instanceof Error ? error.message : "Failed to fetch file content" },
408+
500
409+
);
410+
}
411+
});
412+
258413
export default api;

src/api/github.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,144 @@ export async function createReview(
263263
);
264264
}
265265

266+
// Create a pending review (without submitting)
267+
export async function createPendingReview(
268+
owner: string,
269+
repo: string,
270+
number: number,
271+
commitId: string
272+
): Promise<Review> {
273+
return githubFetch(
274+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews`,
275+
{
276+
method: "POST",
277+
body: JSON.stringify({
278+
commit_id: commitId,
279+
}),
280+
}
281+
);
282+
}
283+
284+
// Add a comment to a pending review
285+
export async function addPendingReviewComment(
286+
owner: string,
287+
repo: string,
288+
number: number,
289+
reviewId: number,
290+
path: string,
291+
line: number,
292+
body: string,
293+
side: "LEFT" | "RIGHT" = "RIGHT",
294+
startLine?: number,
295+
startSide?: "LEFT" | "RIGHT"
296+
): Promise<ReviewComment> {
297+
const payload: Record<string, unknown> = {
298+
body,
299+
path,
300+
line,
301+
side,
302+
};
303+
304+
if (startLine) {
305+
payload.start_line = startLine;
306+
payload.start_side = startSide || side;
307+
}
308+
309+
return githubFetch(
310+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/comments`,
311+
{
312+
method: "POST",
313+
body: JSON.stringify({
314+
...payload,
315+
// Adding to existing pending review - use subject_type
316+
subject_type: "line",
317+
}),
318+
headers: {
319+
// Use preview header to support adding to pending review
320+
Accept: "application/vnd.github.v3+json",
321+
},
322+
}
323+
);
324+
}
325+
326+
// Submit a pending review
327+
export async function submitPendingReview(
328+
owner: string,
329+
repo: string,
330+
number: number,
331+
reviewId: number,
332+
event: "APPROVE" | "REQUEST_CHANGES" | "COMMENT",
333+
body?: string
334+
): Promise<Review> {
335+
return githubFetch(
336+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews/${reviewId}/events`,
337+
{
338+
method: "POST",
339+
body: JSON.stringify({
340+
event,
341+
body: body || "",
342+
}),
343+
}
344+
);
345+
}
346+
347+
// Delete a pending review
348+
export async function deletePendingReview(
349+
owner: string,
350+
repo: string,
351+
number: number,
352+
reviewId: number
353+
): Promise<void> {
354+
await githubFetch(
355+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews/${reviewId}`,
356+
{
357+
method: "DELETE",
358+
}
359+
);
360+
}
361+
362+
// Get comments for a specific review
363+
export async function getReviewComments(
364+
owner: string,
365+
repo: string,
366+
number: number,
367+
reviewId: number
368+
): Promise<ReviewComment[]> {
369+
return githubFetch(
370+
`https://api.github.com/repos/${owner}/${repo}/pulls/${number}/reviews/${reviewId}/comments`
371+
);
372+
}
373+
374+
// Update a review comment
375+
export async function updateReviewComment(
376+
owner: string,
377+
repo: string,
378+
commentId: number,
379+
body: string
380+
): Promise<ReviewComment> {
381+
return githubFetch(
382+
`https://api.github.com/repos/${owner}/${repo}/pulls/comments/${commentId}`,
383+
{
384+
method: "PATCH",
385+
body: JSON.stringify({ body }),
386+
}
387+
);
388+
}
389+
390+
// Delete a review comment
391+
export async function deleteReviewComment(
392+
owner: string,
393+
repo: string,
394+
commentId: number
395+
): Promise<void> {
396+
await githubFetch(
397+
`https://api.github.com/repos/${owner}/${repo}/pulls/comments/${commentId}`,
398+
{
399+
method: "DELETE",
400+
}
401+
);
402+
}
403+
266404
// ============================================================================
267405
// Check Runs & Status
268406
// ============================================================================
@@ -349,3 +487,36 @@ export async function createIssueComment(
349487
);
350488
}
351489

490+
// ============================================================================
491+
// File Content APIs
492+
// ============================================================================
493+
494+
export async function getFileContent(
495+
owner: string,
496+
repo: string,
497+
path: string,
498+
ref: string
499+
): Promise<string> {
500+
const token = getGitHubToken();
501+
const response = await fetch(
502+
`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${ref}`,
503+
{
504+
headers: {
505+
Authorization: `Bearer ${token}`,
506+
Accept: "application/vnd.github.raw+json",
507+
"X-GitHub-Api-Version": "2022-11-28",
508+
},
509+
}
510+
);
511+
512+
if (!response.ok) {
513+
if (response.status === 404) {
514+
return ""; // File doesn't exist at this ref
515+
}
516+
const error = await response.text();
517+
throw new Error(`GitHub API error: ${response.status} - ${error}`);
518+
}
519+
520+
return response.text();
521+
}
522+

0 commit comments

Comments
 (0)