import type { ReviewComment } from "./comments"; import type { CommitEntry } from "./git"; import type { ReviewCheck } from "./review-report"; export type ConversationAction = "fix" | "explain" | "add_test" | "show_alternative"; export interface ChangedLocation { file: string; line?: number; before?: string; after?: string; } export interface AgentResponseInput { commentId: string; summary: string; changedLocations: ChangedLocation[]; checks: ReviewCheck[]; } export interface AgentResponse extends AgentResponseInput { id: string; createdAt: string; } export function setConversationAction( comments: ReviewComment[], commentId: string, action: ConversationAction | null, ): ReviewComment[] { const target = comments.find((comment) => comment.author === "user" && comment.id === commentId); if (!target) throw new Error(`Unknown conversation: ${commentId}`); return comments.map((comment) => comment.id === commentId ? { ...comment, status: "open", requestedAction: action ?? undefined, } : comment, ); } export function applyAgentResponses( comments: ReviewComment[], existing: AgentResponse[], inputs: AgentResponseInput[], createdAt = new Date().toISOString(), ): { comments: ReviewComment[]; responses: AgentResponse[] } { const byId = new Map(comments.filter((comment) => comment.author === "user").map((comment) => [comment.id, comment])); const seen = new Set(); const appended = inputs.map((input, index): AgentResponse => { if (seen.has(input.commentId)) throw new Error(`Duplicate agent response for conversation: ${input.commentId}`); seen.add(input.commentId); const comment = byId.get(input.commentId); if (!comment) throw new Error(`Unknown conversation: ${input.commentId}`); if (comment.status === "resolved") { throw new Error(`Cannot address resolved conversation: ${input.commentId}`); } const summary = input.summary.trim(); if (!summary) throw new Error(`Agent response summary is required for conversation: ${input.commentId}`); return { id: `response-${input.commentId}-${createdAt}-${existing.length + index + 1}`, commentId: input.commentId, summary, changedLocations: input.changedLocations.map((location) => ({ ...location, ...(location.before === undefined && location.file === comment.file && location.line === comment.line && comment.lineText !== undefined ? { before: comment.lineText } : {}), })), checks: input.checks.map((check) => ({ ...check })), createdAt, }; }); const addressed = new Set(inputs.map((input) => input.commentId)); return { comments: comments.map((comment) => addressed.has(comment.id) ? { ...comment, status: "agent_addressed", requestedAction: undefined } : comment, ), responses: [...existing, ...appended], }; } export function responsesForComment(responses: AgentResponse[], commentId: string): AgentResponse[] { return responses.filter((response) => response.commentId === commentId); } export function pruneAgentResponses(responses: AgentResponse[], comments: ReviewComment[]): AgentResponse[] { const commentIds = new Set(comments.map((comment) => comment.id)); return responses.filter((response) => commentIds.has(response.commentId)); } /** Build review items for persisted conversations when the repository has no * current diff. This keeps explanation-only rounds locally reviewable. */ export function conversationReviewItems(comments: ReviewComment[]): CommitEntry[] { const seen = new Set(); const items: CommitEntry[] = []; for (const comment of comments) { if (comment.author !== "user") continue; const key = comment.sha ?? "working"; if (seen.has(key)) continue; seen.add(key); items.push({ sha: comment.sha, label: comment.sha ? comment.sha.slice(0, 7) : "conversation", subject: "Persisted review conversations", }); } return items; }