diff --git a/site/src/pages/AgentsPage/CommentableDiffViewer.tsx b/site/src/pages/AgentsPage/CommentableDiffViewer.tsx new file mode 100644 index 0000000000..2dd8ca2c89 --- /dev/null +++ b/site/src/pages/AgentsPage/CommentableDiffViewer.tsx @@ -0,0 +1,344 @@ +import type { + DiffLineAnnotation, + FileDiffMetadata, + SelectedLineRange, +} from "@pierre/diffs"; +import { Button } from "components/Button/Button"; +import { ArrowUpIcon } from "lucide-react"; +import { + type FC, + type RefObject, + useCallback, + useLayoutEffect, + useRef, + useState, +} from "react"; +import type { ChatMessageInputRef } from "./AgentChatInput"; +import type { DiffStyle } from "./DiffViewer"; +import { DiffViewer } from "./DiffViewer"; +import { + annotationLineForBox, + annotationSideForBox, + type CommentBoxState, + commentBoxFromRange, + contentRangeForBox, + selectedLinesForBox, +} from "./diffCommentSelection"; + +// ------------------------------------------------------------------- +// Diff content extraction +// ------------------------------------------------------------------- + +/** + * Walk the parsed hunks for a file and collect code lines that fall + * within `startLine..endLine` on the given side. For "additions" + * lines are matched against addition line numbers (using + * `hunk.additionStart`); for "deletions" against deletion line + * numbers (using `hunk.deletionStart`). Context lines that fall + * in range are included as well. + */ +export function extractDiffContent( + parsedFiles: readonly FileDiffMetadata[], + fileName: string, + startLine: number, + endLine: number, + side: "additions" | "deletions", +): string { + const file = parsedFiles.find((f) => f.name === fileName); + if (!file) return ""; + + const lines = side === "additions" ? file.additionLines : file.deletionLines; + const collected: string[] = []; + for (const hunk of file.hunks) { + let addLine = hunk.additionStart; + let delLine = hunk.deletionStart; + + for (const block of hunk.hunkContent) { + if (block.type === "context") { + for (let i = 0; i < block.lines; i++) { + const ln = side === "additions" ? addLine : delLine; + if (ln >= startLine && ln <= endLine) { + const idx = + side === "additions" + ? block.additionLineIndex + i + : block.deletionLineIndex + i; + if (lines[idx] != null) collected.push(lines[idx]); + } + addLine++; + delLine++; + } + } else { + // ChangeContent block. + if (side === "deletions") { + for (let i = 0; i < block.deletions; i++) { + if (delLine >= startLine && delLine <= endLine) { + const line = lines[block.deletionLineIndex + i]; + if (line != null) collected.push(line); + } + delLine++; + } + // Addition lines in a change block still advance + // the addition counter. + addLine += block.additions; + } else { + // side === "additions" + // Deletion lines in a change block still advance + // the deletion counter. + delLine += block.deletions; + for (let i = 0; i < block.additions; i++) { + if (addLine >= startLine && addLine <= endLine) { + const line = lines[block.additionLineIndex + i]; + if (line != null) collected.push(line); + } + addLine++; + } + } + } + } + } + + return collected.join("\n"); +} + +// ------------------------------------------------------------------- +// Inline prompt input +// ------------------------------------------------------------------- + +/** + * Inline input rendered as a diff annotation under the selected + * line(s). Supports multiline via Shift+Enter. Enter submits, + * Escape dismisses. + */ +export const InlinePromptInput: FC<{ + onSubmit: (text: string) => void; + onCancel: () => void; +}> = ({ onSubmit, onCancel }) => { + const [text, setText] = useState(""); + const textareaRef = useRef(null); + + useLayoutEffect(() => { + textareaRef.current?.focus(); + }, []); + + return ( +
+
+