Skip to content

Commit ddc3036

Browse files
committed
fetch PR url and support basic comment operations
1 parent beee44e commit ddc3036

File tree

8 files changed

+211
-25
lines changed

8 files changed

+211
-25
lines changed

apps/array/src/main/preload.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
282282
}> => ipcRenderer.invoke("get-diff-stats", repoPath),
283283
getCurrentBranch: (repoPath: string): Promise<string | undefined> =>
284284
ipcRenderer.invoke("get-current-branch", repoPath),
285+
getHeadCommitSha: (repoPath: string): Promise<string> =>
286+
ipcRenderer.invoke("get-head-commit-sha", repoPath),
285287
discardFileChanges: (
286288
repoPath: string,
287289
filePath: string,
@@ -508,4 +510,59 @@ contextBridge.exposeInMainWorld("electronAPI", {
508510
setTerminalLayout: (mode: "split" | "tabbed"): Promise<void> =>
509511
ipcRenderer.invoke("settings:set-terminal-layout", mode),
510512
},
513+
// PR Comments API
514+
prComments: {
515+
getReviewComments: (
516+
directoryPath: string,
517+
prNumber: number,
518+
): Promise<unknown[]> =>
519+
ipcRenderer.invoke("get-pr-review-comments", directoryPath, prNumber),
520+
addComment: (
521+
directoryPath: string,
522+
prNumber: number,
523+
params: {
524+
body: string;
525+
commitId: string;
526+
path: string;
527+
line: number;
528+
side: "LEFT" | "RIGHT";
529+
},
530+
): Promise<unknown> =>
531+
ipcRenderer.invoke("add-pr-comment", directoryPath, prNumber, params),
532+
replyToReview: (
533+
directoryPath: string,
534+
prNumber: number,
535+
params: { body: string; inReplyTo: number },
536+
): Promise<unknown> =>
537+
ipcRenderer.invoke("reply-pr-review", directoryPath, prNumber, params),
538+
updateComment: (
539+
directoryPath: string,
540+
commentId: number,
541+
body: string,
542+
): Promise<unknown> =>
543+
ipcRenderer.invoke("update-pr-comment", directoryPath, commentId, body),
544+
deleteComment: (directoryPath: string, commentId: number): Promise<void> =>
545+
ipcRenderer.invoke("delete-pr-comment", directoryPath, commentId),
546+
resolveComment: (
547+
directoryPath: string,
548+
prNumber: number,
549+
commentId: number,
550+
resolved: boolean,
551+
): Promise<unknown> =>
552+
ipcRenderer.invoke(
553+
"resolve-pr-comment",
554+
directoryPath,
555+
prNumber,
556+
commentId,
557+
resolved,
558+
),
559+
getPrForBranch: (
560+
directoryPath: string,
561+
): Promise<{
562+
number: number;
563+
url: string;
564+
title: string;
565+
state: string;
566+
} | null> => ipcRenderer.invoke("get-pr-for-branch", directoryPath),
567+
},
511568
});

apps/array/src/main/services/git.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,28 @@ const deletePullRequestComment = async (
738738
}
739739
};
740740

741+
interface PullRequestInfo {
742+
number: number;
743+
url: string;
744+
title: string;
745+
state: string;
746+
}
747+
748+
const getPullRequestForBranch = async (
749+
directoryPath: string,
750+
): Promise<PullRequestInfo | null> => {
751+
try {
752+
const { stdout } = await execAsync(
753+
"gh pr view --json number,url,title,state",
754+
{ cwd: directoryPath },
755+
);
756+
return JSON.parse(stdout);
757+
} catch {
758+
// No PR exists for this branch
759+
return null;
760+
}
761+
};
762+
741763
const resolvePullRequestComment = async (
742764
directoryPath: string,
743765
prNumber: number,
@@ -1189,4 +1211,14 @@ export function registerGitIpc(
11891211
);
11901212
},
11911213
);
1214+
1215+
ipcMain.handle(
1216+
"get-pr-for-branch",
1217+
async (
1218+
_event: IpcMainInvokeEvent,
1219+
directoryPath: string,
1220+
): Promise<PullRequestInfo | null> => {
1221+
return getPullRequestForBranch(directoryPath);
1222+
},
1223+
);
11921224
}

apps/array/src/renderer/features/code-editor/components/CodeEditorPanel.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,25 @@ export function CodeEditorPanel({
2626
const repoPath = worktreePath ?? taskData.repoPath;
2727
const filePath = getRelativePath(absolutePath, repoPath);
2828

29-
// Extract PR number from PR URL if available
30-
const prUrl = task.latest_run?.output?.pr_url as string | undefined;
31-
const prNumber = prUrl
32-
? parseInt(prUrl.split("/").pop() || "0", 10)
33-
: undefined;
29+
// Fetch PR for the current branch
30+
const { data: prInfo } = useQuery({
31+
queryKey: ["pr-for-branch", repoPath],
32+
enabled: !!repoPath,
33+
staleTime: 30_000, // Cache for 30 seconds
34+
queryFn: async () => {
35+
if (!window.electronAPI || !repoPath) {
36+
return null;
37+
}
38+
return window.electronAPI.prComments.getPrForBranch(repoPath);
39+
},
40+
});
41+
42+
// Use PR from branch lookup, or fall back to task output
43+
const prUrl =
44+
prInfo?.url ?? (task.latest_run?.output?.pr_url as string | undefined);
45+
const prNumber =
46+
prInfo?.number ??
47+
(prUrl ? parseInt(prUrl.split("/").pop() || "0", 10) : undefined);
3448

3549
const {
3650
data: fileContent,

apps/array/src/renderer/features/code-editor/components/CodeMirrorDiffEditor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ interface CodeMirrorDiffEditorProps {
1111
filePath?: string;
1212
fileId?: string; // Unique identifier for comments (e.g., relative path)
1313
onContentChange?: (content: string) => void;
14+
prNumber?: number;
15+
directoryPath?: string;
1416
}
1517

1618
export function CodeMirrorDiffEditor({
@@ -19,11 +21,15 @@ export function CodeMirrorDiffEditor({
1921
filePath,
2022
fileId,
2123
onContentChange,
24+
prNumber,
25+
directoryPath,
2226
}: CodeMirrorDiffEditorProps) {
2327
const [viewMode, setViewMode] = useState<ViewMode>("split");
2428
const extensions = useEditorExtensions(filePath, true, {
2529
enableComments: true,
2630
fileId: fileId || filePath, // Fall back to filePath if no fileId provided
31+
prNumber,
32+
directoryPath,
2733
});
2834
const options = useMemo(
2935
() => ({

apps/array/src/renderer/features/code-editor/components/DiffEditorPanel.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { PanelMessage } from "@components/ui/PanelMessage";
22
import { CodeMirrorDiffEditor } from "@features/code-editor/components/CodeMirrorDiffEditor";
33
import { CodeMirrorEditor } from "@features/code-editor/components/CodeMirrorEditor";
44
import { getRelativePath } from "@features/code-editor/utils/pathUtils";
5+
import { useCommentStore } from "@features/comments/store/commentStore";
56
import { useTaskData } from "@features/task-detail/hooks/useTaskData";
67
import { Box } from "@radix-ui/themes";
78
import type { Task } from "@shared/types";
89
import { useQuery, useQueryClient } from "@tanstack/react-query";
9-
import { useCallback } from "react";
10+
import { useCallback, useEffect } from "react";
1011
import {
1112
selectWorktreePath,
1213
useWorkspaceStore,
@@ -29,11 +30,40 @@ export function DiffEditorPanel({
2930
const filePath = getRelativePath(absolutePath, repoPath);
3031
const queryClient = useQueryClient();
3132

32-
// Extract PR number from PR URL if available
33-
const prUrl = task.latest_run?.output?.pr_url as string | undefined;
34-
const prNumber = prUrl
35-
? parseInt(prUrl.split("/").pop() || "0", 10)
36-
: undefined;
33+
// Fetch PR for the current branch
34+
const { data: prInfo } = useQuery({
35+
queryKey: ["pr-for-branch", repoPath],
36+
enabled: !!repoPath,
37+
staleTime: 30_000, // Cache for 30 seconds
38+
queryFn: async () => {
39+
if (!window.electronAPI || !repoPath) {
40+
return null;
41+
}
42+
return window.electronAPI.prComments.getPrForBranch(repoPath);
43+
},
44+
});
45+
46+
// Use PR from branch lookup, or fall back to task output
47+
const prUrl =
48+
prInfo?.url ?? (task.latest_run?.output?.pr_url as string | undefined);
49+
const prNumber =
50+
prInfo?.number ??
51+
(prUrl ? parseInt(prUrl.split("/").pop() || "0", 10) : undefined);
52+
53+
// Fetch PR comments when we have a PR number, and refetch on window focus
54+
const fetchComments = useCommentStore((state) => state.fetchComments);
55+
useEffect(() => {
56+
if (!prNumber || !repoPath) {
57+
return;
58+
}
59+
60+
fetchComments(prNumber, repoPath);
61+
62+
// Refetch when window regains focus
63+
const handleFocus = () => fetchComments(prNumber, repoPath);
64+
window.addEventListener("focus", handleFocus);
65+
return () => window.removeEventListener("focus", handleFocus);
66+
}, [prNumber, repoPath, fetchComments]);
3767

3868
const { data: changedFiles = [] } = useQuery({
3969
queryKey: ["changed-files-head", repoPath],
@@ -104,6 +134,8 @@ export function DiffEditorPanel({
104134
filePath={absolutePath}
105135
fileId={filePath} // Use relative path as fileId for comments
106136
onContentChange={handleContentChange}
137+
prNumber={prNumber}
138+
directoryPath={repoPath}
107139
/>
108140
) : (
109141
<CodeMirrorEditor

apps/array/src/renderer/features/code-editor/hooks/useEditorExtensions.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ import { getLanguageExtension } from "../utils/languages";
1616
export function useEditorExtensions(
1717
filePath?: string,
1818
readOnly = false,
19-
options?: { enableComments?: boolean; fileId?: string; prNumber?: number },
19+
options?: {
20+
enableComments?: boolean;
21+
fileId?: string;
22+
prNumber?: number;
23+
directoryPath?: string;
24+
},
2025
) {
2126
const isDarkMode = useThemeStore((state) => state.isDarkMode);
2227
const showComments = useCommentStore((state) => state.showComments);
@@ -28,7 +33,12 @@ export function useEditorExtensions(
2833
const createComment = useCommentStore((state) => state.createComment);
2934
const composerState = useCommentStore((state) => state.composerState);
3035

31-
const { enableComments = false, fileId, prNumber } = options || {};
36+
const {
37+
enableComments = false,
38+
fileId,
39+
prNumber,
40+
directoryPath,
41+
} = options || {};
3242

3343
// Handler for when user clicks "+" to add a comment
3444
const handleOpenComposer = useCallback(
@@ -41,20 +51,28 @@ export function useEditorExtensions(
4151
// Handler for submitting a new comment
4252
const handleSubmitComment = useCallback(
4353
async (line: number, content: string) => {
44-
if (!fileId) return;
54+
if (!fileId || !directoryPath || !prNumber) {
55+
return;
56+
}
57+
58+
try {
59+
// Get the HEAD commit SHA for the comment
60+
const commitId =
61+
await window.electronAPI.getHeadCommitSha(directoryPath);
4562

46-
await createComment({
47-
prNumber: prNumber || 0,
48-
directoryPath: "", // TODO: Get actual directory path from context
49-
path: fileId || "",
50-
line,
51-
side: "right",
52-
content,
53-
commitId: "", // TODO: Get actual commit ID from context
54-
});
55-
closeComposer();
63+
await createComment({
64+
prNumber,
65+
directoryPath,
66+
path: fileId,
67+
line,
68+
side: "right",
69+
content,
70+
commitId,
71+
});
72+
closeComposer();
73+
} catch (_error) {}
5674
},
57-
[fileId, createComment, closeComposer, prNumber],
75+
[fileId, createComment, closeComposer, prNumber, directoryPath],
5876
);
5977

6078
return useMemo(() => {

apps/array/src/renderer/features/comments/store/commentStore.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface CommentStore {
3838
getCommentsForLine: (fileId: string, line: number) => Comment[];
3939

4040
// Commands (async, call API then update local state)
41+
fetchComments: (prNumber: number, directoryPath: string) => Promise<void>;
4142
createComment: (input: CreateCommentInput) => Promise<Comment>;
4243
createReply: (input: CreateReplyInput) => Promise<Comment>;
4344
updateComment: (
@@ -95,6 +96,25 @@ export const useCommentStore = create<CommentStore>()(
9596
// COMMANDS (async, call API + update state)
9697
// ----------------------------------------
9798

99+
fetchComments: async (prNumber: number, directoryPath: string) => {
100+
try {
101+
const comments = await commentApi.fetchComments(
102+
prNumber,
103+
directoryPath,
104+
);
105+
// Group comments by fileId
106+
const commentsByFile: Record<string, Comment[]> = {};
107+
for (const comment of comments) {
108+
if (!commentsByFile[comment.fileId]) {
109+
commentsByFile[comment.fileId] = [];
110+
}
111+
commentsByFile[comment.fileId].push(comment);
112+
}
113+
// Replace all comments with fresh data from API
114+
set({ comments: commentsByFile });
115+
} catch (_error) {}
116+
},
117+
98118
createComment: async (input: CreateCommentInput) => {
99119
// Call API to create comment
100120
const comment = await commentApi.createComment(input);

apps/array/src/renderer/types/electron.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ declare global {
195195
linesRemoved: number;
196196
}>;
197197
getCurrentBranch: (repoPath: string) => Promise<string | undefined>;
198+
getHeadCommitSha: (repoPath: string) => Promise<string>;
198199
discardFileChanges: (
199200
repoPath: string,
200201
filePath: string,
@@ -362,6 +363,12 @@ declare global {
362363
commentId: number,
363364
resolved: boolean,
364365
) => Promise<unknown>;
366+
getPrForBranch: (directoryPath: string) => Promise<{
367+
number: number;
368+
url: string;
369+
title: string;
370+
state: string;
371+
} | null>;
365372
};
366373
}
367374

0 commit comments

Comments
 (0)