import { useMemo, useState } from "react"; import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import type { PRComment } from "@/lib/types/github"; import { CollapsibleSection, AddToContextButton, FeedbackItemRow } from "./pr-shared"; function buildCommentMessage(comment: PRComment, prUrl: string): string { const location = comment.path ? `\`${comment.path}${comment.line > 0 ? `:${comment.line}` : ""}\`` : ""; const header = location ? `Comment from **${comment.author}** on ${location}:` : `Comment from **${comment.author}**:`; return [header, comment.body, `PR: ${prUrl}`, "Please address this comment."].join("\n\n"); } function buildThreadMessage(thread: CommentThread, prUrl: string): string { const parts = ["### Comment Thread", ""]; for (const c of [thread.root, ...thread.replies]) { const loc = c.path ? ` on \`${c.path}${c.line > 0 ? `:${c.line}` : ""}\`` : ""; parts.push(`**${c.author}**${loc}:`); parts.push(c.body); parts.push(""); } parts.push(`PR: ${prUrl}`); parts.push("Please address this comment thread."); return parts.join("\n"); } function buildAllCommentsMessage(comments: PRComment[], prUrl: string): string { const parts = ["### All PR Comments", ""]; for (const c of comments) { const loc = c.path ? ` on \`${c.path}${c.line > 0 ? `:${c.line}` : ""}\`` : ""; parts.push(`**${c.author}**${loc}:`); parts.push(c.body); parts.push(""); } parts.push(`PR: ${prUrl}`); parts.push("Please address the comments above."); return parts.join("\n"); } type CommentThread = { root: PRComment; replies: PRComment[]; }; function buildThreads(comments: PRComment[]): CommentThread[] { const byId = new Map(); for (const c of comments) byId.set(c.id, c); const roots: PRComment[] = []; const repliesByParent = new Map(); for (const c of comments) { if (c.in_reply_to === null || c.in_reply_to === 0 || !byId.has(c.in_reply_to)) { roots.push(c); } else { const existing = repliesByParent.get(c.in_reply_to) ?? []; existing.push(c); repliesByParent.set(c.in_reply_to, existing); } } return roots.map((root) => ({ root, replies: repliesByParent.get(root.id) ?? [], })); } function CommentMetaBadge({ comment, isReply }: { comment: PRComment; isReply?: boolean }) { if (comment.path) { return ( {comment.path} {comment.line > 0 && `:${comment.line}`} ); } if (!isReply) { return (general); } return null; } function CommentItem({ comment, prUrl, onAddAsContext, isReply, }: { comment: PRComment; prUrl: string; onAddAsContext: (message: string) => void; isReply?: boolean; }) { return ( } onAddAsContext={() => onAddAsContext(buildCommentMessage(comment, prUrl))} isReply={isReply} /> ); } function ThreadBlock({ thread, prUrl, onAddAsContext, }: { thread: CommentThread; prUrl: string; onAddAsContext: (message: string) => void; }) { const hasReplies = thread.replies.length > 0; return (
{hasReplies && ( onAddAsContext(buildThreadMessage(thread, prUrl))} tooltip="Add whole thread to chat context" /> )}
{thread.replies.map((reply) => ( ))}
); } export function CommentsSection({ comments, prUrl, onAddAsContext, }: { comments: PRComment[]; prUrl: string; onAddAsContext: (message: string) => void; }) { const [showBotComments, setShowBotComments] = useState(false); const humanComments = useMemo( () => comments.filter((comment) => !comment.author_is_bot), [comments], ); const botComments = useMemo( () => comments.filter((comment) => comment.author_is_bot), [comments], ); const humanThreads = useMemo(() => buildThreads(humanComments), [humanComments]); const botThreads = useMemo(() => buildThreads(botComments), [botComments]); return ( onAddAsContext(buildAllCommentsMessage(comments, prUrl))} addAllLabel="Add all comments to chat context" > {comments.length === 0 && (

No comments yet

)} {humanThreads.map((thread) => ( ))} {botComments.length > 0 && ( )} {showBotComments && botThreads.map((thread) => ( ))}
); }