From 257cf60a356758e668a5396a9d6fdc2bd5a08b92 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 8 Sep 2025 17:28:19 +0100 Subject: [PATCH] Implement git review comments (vibe-kanban) (#654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Perfect! I have successfully implemented the git review comments functionality. Let me provide a summary of what was implemented: ## ✅ Git Review Comments Implementation Complete The implementation leverages git-diff-view's built-in review functionality and integrates it seamlessly with the existing follow-up message system. ### 🔧 **Components Created:** 1. **ReviewProvider** ([`frontend/src/contexts/ReviewProvider.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/contexts/ReviewProvider.tsx)) - Manages comment state across all diff files - Handles draft comments and saved comments - Generates markdown for follow-up messages 2. **CommentWidgetLine** ([`frontend/src/components/diff/CommentWidgetLine.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/diff/CommentWidgetLine.tsx)) - Textarea component for new comments - Auto-focus, keyboard shortcuts (Ctrl+Enter to save, Esc to cancel) - Save/Cancel buttons with validation 3. **ReviewCommentRenderer** ([`frontend/src/components/diff/ReviewCommentRenderer.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/diff/ReviewCommentRenderer.tsx)) - Displays saved comments with delete functionality - Visual styling with comment icon and border 4. **ReviewSubmissionBar** ([`frontend/src/components/diff/ReviewSubmissionBar.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/diff/ReviewSubmissionBar.tsx)) - Sticky bottom bar showing comment count - Submit Review and Discard buttons ### 🔄 **Enhanced Components:** 1. **DiffCard** ([`frontend/src/components/DiffCard.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/DiffCard.tsx)) - Integrated git-diff-view review props - Added comment count badges - Handles click events for adding comments - Renders existing comments inline 2. **DiffTab** ([`frontend/src/components/tasks/TaskDetails/DiffTab.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/tasks/TaskDetails/DiffTab.tsx)) - Wrapped with ReviewProvider - Integrated review submission workflow - Handles follow-up message API calls ### 🎯 **Key Features:** - **GitHub-style inline comments**: Click + button on hover to add comments - **Comment persistence**: Comments stay visible in diff until submitted - **Visual indicators**: Comment count badges on file headers - **Keyboard shortcuts**: Ctrl+Enter to save, Esc to cancel - **Markdown generation**: Comments formatted as review markdown for follow-up messages - **Batch submission**: Submit all comments at once as a single follow-up message ### 🔄 **User Workflow:** 1. User hovers over diff line → **+** button appears 2. Click **+** → Comment textarea opens with auto-focus 3. Type comment → Save with button or Ctrl+Enter 4. Comment appears inline in diff with delete option 5. Continue adding comments across multiple files 6. Submit Review button appears in sticky bottom bar 7. Click Submit Review → All comments sent as markdown follow-up message 8. Comments cleared and follow-up message appears in conversation The implementation is complete and ready for testing. The missing `@ebay/nice-modal-react` dependency is a pre-existing issue unrelated to the review functionality. * ## ✅ Function Signature Update Complete I successfully updated the `handleAddWidgetClick` function signature as requested: ### **Changes Made:** 1. **Updated function signature** in [`DiffCard.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/DiffCard.tsx#L129): ```ts // Before const handleAddWidgetClick = (ctx: any) => { ... } // After const handleAddWidgetClick = (lineNumber: number, side: SplitSide) => { ... } ``` 2. **Imported correct types** from git-diff-view library: ```ts import { DiffModeEnum, DiffView, type SplitSide } from '@git-diff-view/react'; ``` 3. **Updated widget key generation** to use the new parameters: ```ts // Before const widgetKey = `${ctx.filePath}-${ctx.side}-${ctx.insertedLineNumber || ctx.deletedLineNumber}`; // After const widgetKey = `${filePath}-${side}-${lineNumber}`; ``` 4. **Used filePath from component scope** instead of context parameter: - The `filePath` is now captured from the component's scope (`newName || oldName || 'unknown'`) - This is the correct pattern as the git-diff-view library doesn't need to know about repository-specific file paths 5. **Updated renderWidgetLine** to use filePath from scope: ```ts const widgetKey = `${filePath}-${props.side}-${props.lineNumber}`; ``` ### **Key Benefits:** - **Correct API compliance**: Now matches the git-diff-view v0.0.30+ API specification - **Type safety**: Uses proper SplitSide type from the library - **Cleaner separation**: File path logic stays in the UI layer where it belongs - **No functionality lost**: All required data (filePath, side, lineNumber) is still available The implementation has been tested and builds successfully. The function signature now correctly matches `(lineNumber: number, side: SplitSide) => void` as requested. * tweaks * ## ✅ Integration Complete I have successfully integrated the review comments functionality into the existing follow-up system! Here's what was implemented: ### **Key Changes:** 1. **Moved ReviewProvider to TaskDetailsPanel** ([`TaskDetailsPanel.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/tasks/TaskDetailsPanel.tsx#L21)) - Now wraps both DiffTab and TaskFollowUpSection - Ensures review comments are accessible across both components 2. **Removed ReviewSubmissionBar** from DiffTab ([`DiffTab.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/tasks/TaskDetails/DiffTab.tsx)) - Cleaned up separate submission logic - Deleted the now-unused ReviewSubmissionBar component 3. **Enhanced TaskFollowUpSection** ([`TaskFollowUpSection.tsx`](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/vk-ce81-implement/frontend/src/components/tasks/TaskFollowUpSection.tsx#L28)) - Added review comments preview above the textarea - Shows comment count and formatted markdown preview - Dynamic placeholder text based on review comments presence 4. **Unified Submission Logic** - Combines review comments markdown with optional user input - Updates `canSendFollowUp` logic to allow sending with review comments alone - Clears review comments after successful submission ### **User Experience:** - **Review Comments Preview**: When review comments exist, they appear as a formatted preview above the follow-up textarea - **Optional Additional Input**: Users can add extra instructions or leave the textarea empty - **Visual Indicators**: Comment count and MessageSquare icon show when review comments are present - **Single Send Action**: One "Send" button handles both review comments and additional follow-up text - **Contextual Placeholder**: Textarea placeholder changes to indicate additional text is optional when review comments exist ### **Workflow:** 1. User adds review comments in diff files (+ button on hover) 2. Review comments appear as preview in follow-up section 3. User can optionally add additional instructions 4. Single "Send" button submits combined content 5. Both review comments and additional text sent together as one follow-up 6. Comments cleared after successful submission The integration provides a cleaner, unified experience while maintaining all the functionality of both systems. The build passes successfully and the implementation is ready for use! * styling improvements * tsc * SplitSide * turn off useless eslint warning * clear comments btn * dark mode styles * cleanup header * fmt * edit reviews * fmt --- frontend/.eslintrc.json | 7 +- frontend/src/components/DiffCard.tsx | 76 +++++- .../src/components/diff/CommentWidgetLine.tsx | 78 ++++++ .../components/diff/ReviewCommentRenderer.tsx | 107 +++++++++ .../components/tasks/TaskDetails/DiffTab.tsx | 40 ++- .../src/components/tasks/TaskDetailsPanel.tsx | 227 +++++++++--------- .../components/tasks/TaskFollowUpSection.tsx | 127 ++++++---- frontend/src/contexts/ReviewProvider.tsx | 112 +++++++++ 8 files changed, 612 insertions(+), 162 deletions(-) create mode 100644 frontend/src/components/diff/CommentWidgetLine.tsx create mode 100644 frontend/src/components/diff/ReviewCommentRenderer.tsx create mode 100644 frontend/src/contexts/ReviewProvider.tsx diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index e66df566..deb51168 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -25,12 +25,7 @@ "sourceType": "module" }, "rules": { - "react-refresh/only-export-components": [ - "warn", - { - "allowConstantExport": true - } - ], + "react-refresh/only-export-components": "off", "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ "error", diff --git a/frontend/src/components/DiffCard.tsx b/frontend/src/components/DiffCard.tsx index a4fe20b7..c224363b 100644 --- a/frontend/src/components/DiffCard.tsx +++ b/frontend/src/components/DiffCard.tsx @@ -1,5 +1,5 @@ import { Diff } from 'shared/types'; -import { DiffModeEnum, DiffView } from '@git-diff-view/react'; +import { DiffModeEnum, DiffView, SplitSide } from '@git-diff-view/react'; import { generateDiffFile } from '@git-diff-view/file'; import { useMemo } from 'react'; import { useUserSystem } from '@/components/config-provider'; @@ -16,10 +16,14 @@ import { Copy, Key, ExternalLink, + MessageSquare, } from 'lucide-react'; import '@/styles/diff-style-overrides.css'; import { attemptsApi } from '@/lib/api'; import type { TaskAttempt } from 'shared/types'; +import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider'; +import { CommentWidgetLine } from '@/components/diff/CommentWidgetLine'; +import { ReviewCommentRenderer } from '@/components/diff/ReviewCommentRenderer'; type Props = { diff: Diff; @@ -48,6 +52,7 @@ export default function DiffCard({ }: Props) { const { config } = useUserSystem(); const theme = getActualTheme(config?.theme); + const { comments, drafts, setDraft } = useReview(); const oldName = diff.oldPath || undefined; const newName = diff.newPath || oldName || 'unknown'; @@ -94,6 +99,64 @@ export default function DiffCard({ const add = diffFile?.additionLength ?? 0; const del = diffFile?.deletionLength ?? 0; + // Review functionality + const filePath = newName || oldName || 'unknown'; + const commentsForFile = useMemo( + () => comments.filter((c) => c.filePath === filePath), + [comments, filePath] + ); + + // Transform comments to git-diff-view extendData format + const extendData = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const oldFileData: Record = {}; + const newFileData: Record = {}; + + commentsForFile.forEach((comment) => { + const lineKey = String(comment.lineNumber); + if (comment.side === SplitSide.old) { + oldFileData[lineKey] = { data: comment }; + } else { + newFileData[lineKey] = { data: comment }; + } + }); + + return { + oldFile: oldFileData, + newFile: newFileData, + }; + }, [commentsForFile]); + + const handleAddWidgetClick = (lineNumber: number, side: SplitSide) => { + const widgetKey = `${filePath}-${side}-${lineNumber}`; + const draft: ReviewDraft = { + filePath, + side, + lineNumber, + text: '', + }; + setDraft(widgetKey, draft); + }; + + const renderWidgetLine = (props: any) => { + const widgetKey = `${filePath}-${props.side}-${props.lineNumber}`; + const draft = drafts[widgetKey]; + if (!draft) return null; + + return ( + + ); + }; + + const renderExtendLine = (lineData: any) => { + return ; + }; + // Title row const title = (

-{del} + {commentsForFile.length > 0 && ( + + + {commentsForFile.length} + + )}

); @@ -180,6 +249,11 @@ export default function DiffCard({ diffViewHighlight diffViewMode={DiffModeEnum.Unified} diffViewFontSize={12} + diffViewAddWidget + onAddWidgetClick={handleAddWidgetClick} + renderWidgetLine={renderWidgetLine} + extendData={extendData} + renderExtendLine={renderExtendLine} /> )} diff --git a/frontend/src/components/diff/CommentWidgetLine.tsx b/frontend/src/components/diff/CommentWidgetLine.tsx new file mode 100644 index 00000000..7d38d05d --- /dev/null +++ b/frontend/src/components/diff/CommentWidgetLine.tsx @@ -0,0 +1,78 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider'; + +interface CommentWidgetLineProps { + draft: ReviewDraft; + widgetKey: string; + onSave: () => void; + onCancel: () => void; +} + +export function CommentWidgetLine({ + draft, + widgetKey, + onSave, + onCancel, +}: CommentWidgetLineProps) { + const { setDraft, addComment } = useReview(); + const [value, setValue] = useState(draft.text); + const textareaRef = useRef(null); + + useEffect(() => { + textareaRef.current?.focus(); + }, []); + + const handleSave = () => { + if (value.trim()) { + addComment({ + filePath: draft.filePath, + side: draft.side, + lineNumber: draft.lineNumber, + text: value.trim(), + }); + } + setDraft(widgetKey, null); + onSave(); + }; + + const handleCancel = () => { + setDraft(widgetKey, null); + onCancel(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + handleCancel(); + } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + handleSave(); + } + }; + + return ( +
+