@@ -19,8 +19,20 @@ import {
1919 getIssueComments ,
2020 createIssueComment ,
2121 getFileContent ,
22+ getCurrentUser ,
23+ // GraphQL mutations for pending reviews
24+ getPullRequestNodeId ,
25+ getPendingReviewNodeId ,
26+ addPendingReviewCommentGraphQL ,
27+ deletePendingReviewCommentGraphQL ,
28+ updatePendingReviewCommentGraphQL ,
29+ submitPendingReviewGraphQL ,
30+ // Review thread resolution
31+ getReviewThreads ,
32+ resolveReviewThread ,
33+ unresolveReviewThread ,
2234} from "./github" ;
23- import { parseDiffWithHighlighting } from "./diff" ;
35+ import { parseDiffWithHighlighting , highlightFileLines } from "./diff" ;
2436
2537const api = new Hono ( ) . basePath ( "/api" ) ;
2638
@@ -52,16 +64,43 @@ api.get("/pr/:owner/:repo/:number/files", async (c) => {
5264 }
5365} ) ;
5466
55- // Get PR comments
67+ // Get PR comments (with thread resolution info from GraphQL)
5668api . get ( "/pr/:owner/:repo/:number/comments" , async ( c ) => {
5769 const { owner, repo, number } = c . req . param ( ) ;
5870 try {
59- const comments = await getPullRequestComments (
60- owner ,
61- repo ,
62- parseInt ( number , 10 )
63- ) ;
64- return c . json ( comments ) ;
71+ // Fetch both REST comments and GraphQL threads for resolution info
72+ const [ comments , threads ] = await Promise . all ( [
73+ getPullRequestComments ( owner , repo , parseInt ( number , 10 ) ) ,
74+ getReviewThreads ( owner , repo , parseInt ( number , 10 ) ) ,
75+ ] ) ;
76+
77+ // Create a map of comment database ID to thread info
78+ const commentToThread = new Map < number , { threadId : string ; isResolved : boolean ; resolvedBy : { login : string ; avatar_url : string } | null } > ( ) ;
79+ for ( const thread of threads ) {
80+ for ( const comment of thread . comments ) {
81+ commentToThread . set ( comment . databaseId , {
82+ threadId : thread . id ,
83+ isResolved : thread . isResolved ,
84+ resolvedBy : thread . resolvedBy ? {
85+ login : thread . resolvedBy . login ,
86+ avatar_url : thread . resolvedBy . avatarUrl ,
87+ } : null ,
88+ } ) ;
89+ }
90+ }
91+
92+ // Enrich REST comments with thread info
93+ const enrichedComments = comments . map ( ( comment ) => {
94+ const threadInfo = commentToThread . get ( comment . id ) ;
95+ return {
96+ ...comment ,
97+ pull_request_review_thread_id : threadInfo ?. threadId ,
98+ is_resolved : threadInfo ?. isResolved ?? false ,
99+ resolved_by : threadInfo ?. resolvedBy ?? null ,
100+ } ;
101+ } ) ;
102+
103+ return c . json ( enrichedComments ) ;
65104 } catch ( error ) {
66105 return c . json (
67106 { error : error instanceof Error ? error . message : "Failed to fetch comments" } ,
@@ -70,6 +109,48 @@ api.get("/pr/:owner/:repo/:number/comments", async (c) => {
70109 }
71110} ) ;
72111
112+ // Get review threads (GraphQL - includes resolution status)
113+ api . get ( "/pr/:owner/:repo/:number/threads" , async ( c ) => {
114+ const { owner, repo, number } = c . req . param ( ) ;
115+ try {
116+ const threads = await getReviewThreads ( owner , repo , parseInt ( number , 10 ) ) ;
117+ return c . json ( threads ) ;
118+ } catch ( error ) {
119+ return c . json (
120+ { error : error instanceof Error ? error . message : "Failed to fetch threads" } ,
121+ 500
122+ ) ;
123+ }
124+ } ) ;
125+
126+ // Resolve a review thread
127+ api . post ( "/pr/:owner/:repo/:number/threads/:threadId/resolve" , async ( c ) => {
128+ const { threadId } = c . req . param ( ) ;
129+ try {
130+ await resolveReviewThread ( threadId ) ;
131+ return c . json ( { success : true } ) ;
132+ } catch ( error ) {
133+ return c . json (
134+ { error : error instanceof Error ? error . message : "Failed to resolve thread" } ,
135+ 500
136+ ) ;
137+ }
138+ } ) ;
139+
140+ // Unresolve a review thread
141+ api . post ( "/pr/:owner/:repo/:number/threads/:threadId/unresolve" , async ( c ) => {
142+ const { threadId } = c . req . param ( ) ;
143+ try {
144+ await unresolveReviewThread ( threadId ) ;
145+ return c . json ( { success : true } ) ;
146+ } catch ( error ) {
147+ return c . json (
148+ { error : error instanceof Error ? error . message : "Failed to unresolve thread" } ,
149+ 500
150+ ) ;
151+ }
152+ } ) ;
153+
73154// Create PR comment
74155api . post ( "/pr/:owner/:repo/:number/comments" , async ( c ) => {
75156 const { owner, repo, number } = c . req . param ( ) ;
@@ -177,7 +258,7 @@ api.post("/pr/:owner/:repo/:number/reviews", async (c) => {
177258 }
178259} ) ;
179260
180- // Create a pending review
261+ // Create a pending review (REST API - legacy)
181262api . post ( "/pr/:owner/:repo/:number/reviews/pending" , async ( c ) => {
182263 const { owner, repo, number } = c . req . param ( ) ;
183264 const body = await c . req . json ( ) ;
@@ -198,6 +279,110 @@ api.post("/pr/:owner/:repo/:number/reviews/pending", async (c) => {
198279 }
199280} ) ;
200281
282+ // Get PR node ID (for GraphQL mutations)
283+ api . get ( "/pr/:owner/:repo/:number/node-id" , async ( c ) => {
284+ const { owner, repo, number } = c . req . param ( ) ;
285+
286+ try {
287+ const nodeId = await getPullRequestNodeId ( owner , repo , parseInt ( number , 10 ) ) ;
288+ return c . json ( { nodeId } ) ;
289+ } catch ( error ) {
290+ return c . json (
291+ { error : error instanceof Error ? error . message : "Failed to get PR node ID" } ,
292+ 500
293+ ) ;
294+ }
295+ } ) ;
296+
297+ // Get user's pending review via GraphQL
298+ api . get ( "/pr/:owner/:repo/:number/pending-review" , async ( c ) => {
299+ const { owner, repo, number } = c . req . param ( ) ;
300+
301+ try {
302+ const result = await getPendingReviewNodeId ( owner , repo , parseInt ( number , 10 ) ) ;
303+ return c . json ( result ) ;
304+ } catch ( error ) {
305+ return c . json (
306+ { error : error instanceof Error ? error . message : "Failed to get pending review" } ,
307+ 500
308+ ) ;
309+ }
310+ } ) ;
311+
312+ // Add a comment to pending review via GraphQL (creates review if needed)
313+ api . post ( "/pr/:owner/:repo/:number/pending-comment" , async ( c ) => {
314+ const { owner, repo, number } = c . req . param ( ) ;
315+ const body = await c . req . json ( ) ;
316+
317+ try {
318+ // Get PR node ID if not provided
319+ let pullRequestId = body . pull_request_id ;
320+ if ( ! pullRequestId ) {
321+ pullRequestId = await getPullRequestNodeId ( owner , repo , parseInt ( number , 10 ) ) ;
322+ }
323+
324+ const result = await addPendingReviewCommentGraphQL (
325+ pullRequestId ,
326+ body . path ,
327+ body . line ,
328+ body . body ,
329+ body . start_line
330+ ) ;
331+ return c . json ( result ) ;
332+ } catch ( error ) {
333+ return c . json (
334+ { error : error instanceof Error ? error . message : "Failed to add pending comment" } ,
335+ 500
336+ ) ;
337+ }
338+ } ) ;
339+
340+ // Delete a pending review comment via GraphQL
341+ api . delete ( "/pr/:owner/:repo/:number/pending-comment/:commentId" , async ( c ) => {
342+ const { commentId } = c . req . param ( ) ;
343+
344+ try {
345+ await deletePendingReviewCommentGraphQL ( commentId ) ;
346+ return c . json ( { success : true } ) ;
347+ } catch ( error ) {
348+ return c . json (
349+ { error : error instanceof Error ? error . message : "Failed to delete pending comment" } ,
350+ 500
351+ ) ;
352+ }
353+ } ) ;
354+
355+ // Update a pending review comment via GraphQL
356+ api . patch ( "/pr/:owner/:repo/:number/pending-comment/:commentId" , async ( c ) => {
357+ const { commentId } = c . req . param ( ) ;
358+ const body = await c . req . json ( ) ;
359+
360+ try {
361+ await updatePendingReviewCommentGraphQL ( commentId , body . body ) ;
362+ return c . json ( { success : true } ) ;
363+ } catch ( error ) {
364+ return c . json (
365+ { error : error instanceof Error ? error . message : "Failed to update pending comment" } ,
366+ 500
367+ ) ;
368+ }
369+ } ) ;
370+
371+ // Submit pending review via GraphQL
372+ api . post ( "/pr/:owner/:repo/:number/pending-review/submit" , async ( c ) => {
373+ const body = await c . req . json ( ) ;
374+
375+ try {
376+ await submitPendingReviewGraphQL ( body . review_id , body . event , body . body ) ;
377+ return c . json ( { success : true } ) ;
378+ } catch ( error ) {
379+ return c . json (
380+ { error : error instanceof Error ? error . message : "Failed to submit review" } ,
381+ 500
382+ ) ;
383+ }
384+ } ) ;
385+
201386// Submit a pending review
202387api . post ( "/pr/:owner/:repo/:number/reviews/:reviewId/submit" , async ( c ) => {
203388 const { owner, repo, number, reviewId } = c . req . param ( ) ;
@@ -410,4 +595,40 @@ api.get("/file/:owner/:repo", async (c) => {
410595 }
411596} ) ;
412597
598+ // Highlight file lines (for skip block expansion)
599+ api . post ( "/highlight-lines" , async ( c ) => {
600+ try {
601+ const body = await c . req . json ( ) ;
602+ const { content, filename, startLine, count } = body ;
603+
604+ if ( ! content || ! filename || ! startLine || ! count ) {
605+ return c . json ( { error : "Missing required fields" } , 400 ) ;
606+ }
607+
608+ const lines = highlightFileLines ( content , filename , startLine , count ) ;
609+ return c . json ( lines ) ;
610+ } catch ( error ) {
611+ return c . json (
612+ { error : error instanceof Error ? error . message : "Failed to highlight lines" } ,
613+ 500
614+ ) ;
615+ }
616+ } ) ;
617+
618+ // ============================================================================
619+ // User
620+ // ============================================================================
621+
622+ api . get ( "/user" , async ( c ) => {
623+ try {
624+ const user = await getCurrentUser ( ) ;
625+ return c . json ( user ) ;
626+ } catch ( error ) {
627+ return c . json (
628+ { error : error instanceof Error ? error . message : "Failed to fetch user" } ,
629+ 500
630+ ) ;
631+ }
632+ } ) ;
633+
413634export default api ;
0 commit comments