From 4a0fad03c25c1973e46baa8bd7e01ef5d760427a Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 12 Jan 2026 23:47:53 +0000 Subject: [PATCH] Display GitHub PR comments inline in diff view (Vibe Kanban) (#1991) * The GitHub comments integration is now complete. Here's a summary of what was implemented: ## Summary The GitHub PR comments integration for the new UI (ui-new) is now complete. All tasks have been implemented: ### Files Modified: 1. **`useUiPreferencesStore.ts`** - Added persistence key for `showGitHubComments` toggle 2. **`WorkspaceContext.tsx`** - Extended with PR comments data, helpers (`getGitHubCommentsForFile`, `gitHubFileCommentCounts`), and toggle state 3. **`CommentCard.tsx`** (new) - Shared primitive for displaying comments with three variants: `user`, `github`, `input` 4. **`GitHubCommentRenderer.tsx`** (new) - Read-only renderer for GitHub comments with GitHub logo, author, timestamp, and external link 5. **`ReviewCommentRenderer.tsx`** - Refactored to use `CommentCard` primitive 6. **`CommentWidgetLine.tsx`** - Refactored to use `CommentCard` primitive 7. **`DiffViewCardWithComments.tsx`** - Integrated GitHub comments into diff view, showing both user and GitHub comments with separate counts in the header 8. **`FileTreeNode.tsx`** - Added GitHub comment badge showing count next to files 9. **`FileTree.tsx`** - Added toggle button in header to show/hide GitHub comments 10. **`FileTreeContainer.tsx`** - Wired up the context to pass toggle state and comment counts to FileTree 11. **`common.json`** (i18n) - Added translation keys for show/hide GitHub comments ### Features: - GitHub PR comments displayed inline next to relevant code in diff views - Toggle button in file tree to show/hide GitHub comments (persisted) - Comment count badges on files in the file tree - Visual differentiation: user comments have orange styling (`bg-brand/20 border-brand`), GitHub comments have neutral styling (`bg-secondary border-border`) - User comments take priority when both exist on the same line * Cleanup script changes for workspace 881e2956-9cca-406d-835f-7ea8acbe550e * All checks pass. Let me show a summary of the changes made: ## Summary of Fixes All code review feedback has been addressed: | # | Fix | File | Change | |---|-----|------|--------| | 1 | Date parsing | `GitHubCommentRenderer.tsx:9-27` | Replaced `try/catch` with `isNaN(date.getTime())` check | | 2 | Comment priority | `DiffViewCardWithComments.tsx:258-260` | Added comment explaining user comments take priority | | 3 | SplitSide.new | `WorkspaceContext.tsx:163-165` | Added comment explaining why `SplitSide.new` is correct | | 4 | Multi-repo TODO | `WorkspaceContext.tsx:133-134` | Added TODO about supporting multiple repos | | 5 | Loading state | `FileTreeContainer.tsx`, `FileTree.tsx` | Passed `isGitHubCommentsLoading` prop and added `opacity-50 animate-pulse` to button | Type checks pass. Ready for commit. * Fixed! The `FileTree.tsx` now uses the correct `Tooltip` component from `@/components/ui-new/primitives/Tooltip` instead of the old `@/components/ui/tooltip`. Changes made: - Updated import to use `import { Tooltip } from '../primitives/Tooltip'` - Simplified the tooltip usage from the compound component pattern (`Tooltip` + `TooltipTrigger` + `TooltipContent`) to the simpler API (`...`) * The path matching bug has been fixed. Here's a summary of the changes: ## Summary of Bug Fix **Problem**: GitHub comments weren't displaying because paths didn't match. - GitHub API returns paths like: `frontend/src/components/ui-new/actions/index.ts` - Diff paths are prefixed with repo name: `vibe-kanban/frontend/src/components/ui-new/actions/index.ts` **Solution**: Use `endsWith` matching instead of exact string equality. ### Files Changed: 1. **`WorkspaceContext.tsx`**: - Added `pathMatches` helper function that checks if paths match using `endsWith` - Updated `getGitHubCommentsForFile` to use `pathMatches` - Replaced `gitHubFileCommentCounts: Record` with `getGitHubCommentCountForFile: (filePath: string) => number` function that also uses `pathMatches` 2. **`FileTree.tsx`**: - Changed prop from `fileCommentCounts?: Record` to `getGitHubCommentCountForFile?: (filePath: string) => number` - Updated usage from `fileCommentCounts?.[node.path]` to `getGitHubCommentCountForFile?.(node.path)` 3. **`FileTreeContainer.tsx`**: - Updated to pass the new `getGitHubCommentCountForFile` function instead of `gitHubFileCommentCounts` Record * Cleanup script changes for workspace 881e2956-9cca-406d-835f-7ea8acbe550e * Fixed! The `renderExtendLine` function now includes a null check for `lineData.data` to prevent the TypeError when switching diff modes. The fix adds: ```typescript // Guard against undefined data (can happen when switching diff modes) if (!lineData.data) return null; ``` This prevents the crash when the `@git-diff-view` library passes undefined data during mode transitions. * Done! GitHub comments will now be shown by default. The change was a simple one-liner in `WorkspaceContext.tsx`: ```typescript // Before: false // Default to hidden // After: true // Default to shown ``` * The proper fix is complete. Here's a summary of the changes: ## Summary: Comments on deleted lines fix **Problem**: GitHub comments on deleted lines weren't showing because we were placing all comments on `SplitSide.new`. **Solution**: Pass through the `side` field from GitHub's API which indicates whether the comment is on the "LEFT" (old/deleted) or "RIGHT" (new/added) side. ### Files Changed: 1. **`crates/services/src/services/git_host/types.rs`** - Added `side: Option` field to `UnifiedPrComment::Review` variant 2. **`crates/services/src/services/git_host/github/mod.rs`** - Pass `c.side` through when creating `UnifiedPrComment::Review` 3. **`crates/services/src/services/git_host/azure/cli.rs`** - Added `side: None` to Azure DevOps (doesn't have this info) 4. **`shared/types.ts`** (regenerated) - Now includes `side: string | null` in the review comment type 5. **`frontend/src/contexts/WorkspaceContext.tsx`** - Updated normalization to use `comment.side === 'LEFT' ? SplitSide.old : SplitSide.new` 6. **`frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx`** - Updated `extendData` building to place GitHub comments on the correct side (`oldFileData` or `newFileData`) based on `comment.side` * The refactoring is complete. Here's a summary of the changes: **Created new file:** `frontend/src/hooks/useGitHubComments.ts` - Contains the `NormalizedGitHubComment` interface - Contains all GitHub comments logic: fetching, toggle state, normalization, path matching helpers **Updated:** `frontend/src/contexts/WorkspaceContext.tsx` - Removed ~70 lines of GitHub comments logic - Now imports and uses `useGitHubComments` hook - Re-exports `NormalizedGitHubComment` type for consumers that import it from the context The context file is now smaller and more focused on workspace management, while the GitHub comments functionality is encapsulated in its own dedicated hook. * Cleanup script changes for workspace 881e2956-9cca-406d-835f-7ea8acbe550e * Done. Removed the duplicate `formatRelativeTime` function from `GitHubCommentRenderer.tsx` and replaced it with an import from the existing `@/utils/date` utility. * The i18n check now passes. All translation keys are consistent across locales. I added the missing `showGitHubComments` and `hideGitHubComments` translations to: - Spanish (es) - Japanese (ja) - Korean (ko) - Simplified Chinese (zh-Hans) - Traditional Chinese (zh-Hant) --- .../src/services/git_host/azure/cli.rs | 1 + .../src/services/git_host/github/mod.rs | 1 + .../services/src/services/git_host/types.rs | 1 + .../ui-new/containers/CommentWidgetLine.tsx | 33 ++--- .../containers/DiffViewCardWithComments.tsx | 103 ++++++++++++--- .../ui-new/containers/FileTreeContainer.tsx | 13 ++ .../containers/GitHubCommentRenderer.tsx | 41 ++++++ .../containers/ReviewCommentRenderer.tsx | 53 ++++---- .../ui-new/primitives/CommentCard.tsx | 51 ++++++++ .../src/components/ui-new/views/FileTree.tsx | 58 ++++++++- .../components/ui-new/views/FileTreeNode.tsx | 27 +++- frontend/src/contexts/WorkspaceContext.tsx | 44 +++++++ frontend/src/hooks/useGitHubComments.ts | 122 ++++++++++++++++++ frontend/src/i18n/locales/en/common.json | 4 +- frontend/src/i18n/locales/es/common.json | 4 +- frontend/src/i18n/locales/ja/common.json | 4 +- frontend/src/i18n/locales/ko/common.json | 4 +- frontend/src/i18n/locales/zh-Hans/common.json | 4 +- frontend/src/i18n/locales/zh-Hant/common.json | 4 +- frontend/src/stores/useUiPreferencesStore.ts | 3 + shared/types.ts | 2 +- 21 files changed, 504 insertions(+), 73 deletions(-) create mode 100644 frontend/src/components/ui-new/containers/GitHubCommentRenderer.tsx create mode 100644 frontend/src/components/ui-new/primitives/CommentCard.tsx create mode 100644 frontend/src/hooks/useGitHubComments.ts diff --git a/crates/services/src/services/git_host/azure/cli.rs b/crates/services/src/services/git_host/azure/cli.rs index cb5ad420..4e4fda78 100644 --- a/crates/services/src/services/git_host/azure/cli.rs +++ b/crates/services/src/services/git_host/azure/cli.rs @@ -531,6 +531,7 @@ impl AzCli { url: None, path: path.clone(), line, + side: None, diff_hunk: None, }); } else { diff --git a/crates/services/src/services/git_host/github/mod.rs b/crates/services/src/services/git_host/github/mod.rs index ec75f227..867460c4 100644 --- a/crates/services/src/services/git_host/github/mod.rs +++ b/crates/services/src/services/git_host/github/mod.rs @@ -352,6 +352,7 @@ impl GitHostProvider for GitHubProvider { url: Some(c.html_url), path: c.path, line: c.line, + side: c.side, diff_hunk: Some(c.diff_hunk), }); } diff --git a/crates/services/src/services/git_host/types.rs b/crates/services/src/services/git_host/types.rs index a347e52d..52b5a393 100644 --- a/crates/services/src/services/git_host/types.rs +++ b/crates/services/src/services/git_host/types.rs @@ -119,6 +119,7 @@ pub enum UnifiedPrComment { url: Option, path: String, line: Option, + side: Option, diff_hunk: Option, }, } diff --git a/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx b/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx index f882d715..d8e24e7d 100644 --- a/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx +++ b/frontend/src/components/ui-new/containers/CommentWidgetLine.tsx @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PrimaryButton } from '../primitives/PrimaryButton'; +import { CommentCard } from '../primitives/CommentCard'; import WYSIWYGEditor from '@/components/ui/wysiwyg'; import { useReview, type ReviewDraft } from '@/contexts/ReviewProvider'; @@ -43,18 +44,10 @@ export function CommentWidgetLine({ }, [value, draft, setDraft, widgetKey, onSave, addComment]); return ( -
-
- -
+ {t('actions.cancel')} -
-
-
+ + } + > + + ); } diff --git a/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx b/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx index e62bdfb7..ab38d739 100644 --- a/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx +++ b/frontend/src/components/ui-new/containers/DiffViewCardWithComments.tsx @@ -1,6 +1,10 @@ import { useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { CaretDownIcon, ChatCircleIcon } from '@phosphor-icons/react'; +import { + CaretDownIcon, + ChatCircleIcon, + GithubLogoIcon, +} from '@phosphor-icons/react'; import { DiffView, DiffModeEnum, SplitSide } from '@git-diff-view/react'; import { generateDiffFile, type DiffFile } from '@git-diff-view/file'; import { cn } from '@/lib/utils'; @@ -15,8 +19,13 @@ import { type ReviewDraft, type ReviewComment, } from '@/contexts/ReviewProvider'; +import { + useWorkspaceContext, + type NormalizedGitHubComment, +} from '@/contexts/WorkspaceContext'; import { CommentWidgetLine } from './CommentWidgetLine'; import { ReviewCommentRenderer } from './ReviewCommentRenderer'; +import { GitHubCommentRenderer } from './GitHubCommentRenderer'; import type { ToolStatus, DiffChangeKind } from 'shared/types'; import { ToolStatusDot } from '../primitives/conversation/ToolStatusDot'; import { OpenInIdeButton } from '@/components/ide/OpenInIdeButton'; @@ -24,6 +33,11 @@ import { useOpenInEditor } from '@/hooks/useOpenInEditor'; import '@/styles/diff-style-overrides.css'; import { DisplayTruncatedPath } from '@/utils/TruncatePath'; +/** Discriminated union for comment data in extendData */ +type ExtendLineData = + | { type: 'review'; comment: ReviewComment } + | { type: 'github'; comment: NormalizedGitHubComment }; + // Discriminated union for input format flexibility export type DiffInput = | { @@ -172,6 +186,8 @@ export function DiffViewCardWithComments({ const { diffFile, additions, deletions, filePath, isValid } = useDiffData(input); const { comments, drafts, setDraft } = useReview(); + const { showGitHubComments, getGitHubCommentsForFile } = + useWorkspaceContext(); // Open in IDE functionality const openInEditor = useOpenInEditor(attemptId); @@ -212,17 +228,48 @@ export function DiffViewCardWithComments({ [comments, filePath] ); - // Transform comments to git-diff-view extendData format - const extendData = useMemo(() => { - const oldFileData: Record = {}; - const newFileData: Record = {}; + // Get GitHub comments for this file (only when enabled) + const githubCommentsForFile = useMemo(() => { + if (!showGitHubComments) return []; + return getGitHubCommentsForFile(filePath); + }, [showGitHubComments, getGitHubCommentsForFile, filePath]); + // Total comment count (user + GitHub) + const totalCommentCount = + commentsForFile.length + githubCommentsForFile.length; + + // Transform comments to git-diff-view extendData format + // The library expects { data: T } where T is the actual data + const extendData = useMemo(() => { + const oldFileData: Record = {}; + const newFileData: Record = {}; + + // Add user review comments first (higher priority) commentsForFile.forEach((comment) => { const lineKey = String(comment.lineNumber); + const entry: ExtendLineData = { type: 'review', comment }; if (comment.side === SplitSide.old) { - oldFileData[lineKey] = { data: comment }; + oldFileData[lineKey] = { data: entry }; } else { - newFileData[lineKey] = { data: comment }; + newFileData[lineKey] = { data: entry }; + } + }); + + // Add GitHub comments (only if no user comment on that line). + // User comments take priority - if you're adding your own comment on a line, + // you've likely addressed the GitHub feedback, so we hide the GitHub comment. + githubCommentsForFile.forEach((comment) => { + const lineKey = String(comment.lineNumber); + const entry: ExtendLineData = { type: 'github', comment }; + // Place comment on correct side based on GitHub's side field + if (comment.side === SplitSide.old) { + if (!oldFileData[lineKey]) { + oldFileData[lineKey] = { data: entry }; + } + } else { + if (!newFileData[lineKey]) { + newFileData[lineKey] = { data: entry }; + } } }); @@ -230,7 +277,7 @@ export function DiffViewCardWithComments({ oldFile: oldFileData, newFile: newFileData, }; - }, [commentsForFile]); + }, [commentsForFile, githubCommentsForFile]); // Handle click on "add widget" button in diff view const handleAddWidgetClick = useCallback( @@ -269,11 +316,21 @@ export function DiffViewCardWithComments({ [filePath, drafts, projectId] ); - // Render existing comments below lines + // Render existing comments below lines (handles both user and GitHub comments) + // The library wraps our data in { data: ExtendLineData } const renderExtendLine = useCallback( - (lineData: { data: ReviewComment }) => { + (lineData: { data: ExtendLineData }) => { + // Guard against undefined data (can happen when switching diff modes) + if (!lineData.data) return null; + + if (lineData.data.type === 'github') { + return ; + } return ( - + ); }, [projectId] @@ -332,10 +389,20 @@ export function DiffViewCardWithComments({ {deletions > 0 && -{deletions}} )} - {commentsForFile.length > 0 && ( - - - {commentsForFile.length} + {totalCommentCount > 0 && ( + + {commentsForFile.length > 0 && ( + + + {commentsForFile.length} + + )} + {githubCommentsForFile.length > 0 && ( + + + {githubCommentsForFile.length} + + )} )}
@@ -393,8 +460,8 @@ function DiffViewBodyWithComments({ theme: 'light' | 'dark'; diffMode: DiffModeEnum; extendData: { - oldFile: Record; - newFile: Record; + oldFile: Record; + newFile: Record; }; onAddWidgetClick: (lineNumber: number, side: SplitSide) => void; renderWidgetLine: (props: { @@ -402,7 +469,7 @@ function DiffViewBodyWithComments({ lineNumber: number; onClose: () => void; }) => React.ReactNode; - renderExtendLine: (lineData: { data: ReviewComment }) => React.ReactNode; + renderExtendLine: (lineData: { data: ExtendLineData }) => React.ReactNode; }) { const { t } = useTranslation('tasks'); if (!isValid || !diffFile) { diff --git a/frontend/src/components/ui-new/containers/FileTreeContainer.tsx b/frontend/src/components/ui-new/containers/FileTreeContainer.tsx index 6b09144f..211d1db9 100644 --- a/frontend/src/components/ui-new/containers/FileTreeContainer.tsx +++ b/frontend/src/components/ui-new/containers/FileTreeContainer.tsx @@ -7,6 +7,7 @@ import { getAllFolderPaths, } from '@/utils/fileTreeUtils'; import { usePersistedCollapsedPaths } from '@/stores/useUiPreferencesStore'; +import { useWorkspaceContext } from '@/contexts/WorkspaceContext'; import type { Diff } from 'shared/types'; interface FileTreeContainerProps { @@ -30,6 +31,14 @@ export function FileTreeContainer({ const [selectedPath, setSelectedPath] = useState(null); const nodeRefs = useRef>(new Map()); + // Get GitHub comments state from workspace context + const { + showGitHubComments, + setShowGitHubComments, + getGitHubCommentCountForFile, + isGitHubCommentsLoading, + } = useWorkspaceContext(); + // Sync selectedPath with external selectedFilePath prop and scroll into view useEffect(() => { if (selectedFilePath !== undefined) { @@ -129,6 +138,10 @@ export function FileTreeContainer({ isAllExpanded={isAllExpanded} onToggleExpandAll={handleToggleExpandAll} className={className} + showGitHubComments={showGitHubComments} + onToggleGitHubComments={setShowGitHubComments} + getGitHubCommentCountForFile={getGitHubCommentCountForFile} + isGitHubCommentsLoading={isGitHubCommentsLoading} /> ); } diff --git a/frontend/src/components/ui-new/containers/GitHubCommentRenderer.tsx b/frontend/src/components/ui-new/containers/GitHubCommentRenderer.tsx new file mode 100644 index 00000000..901356e8 --- /dev/null +++ b/frontend/src/components/ui-new/containers/GitHubCommentRenderer.tsx @@ -0,0 +1,41 @@ +import { GithubLogoIcon, ArrowSquareOutIcon } from '@phosphor-icons/react'; +import { CommentCard } from '../primitives/CommentCard'; +import { formatRelativeTime } from '@/utils/date'; +import type { NormalizedGitHubComment } from '@/contexts/WorkspaceContext'; + +interface GitHubCommentRendererProps { + comment: NormalizedGitHubComment; +} + +/** + * Read-only renderer for GitHub PR comments. + * Uses CommentCard primitive with 'github' variant for neutral styling. + */ +export function GitHubCommentRenderer({ comment }: GitHubCommentRendererProps) { + const header = ( +
+ + @{comment.author} + {formatRelativeTime(comment.createdAt)} + {comment.url && ( + e.stopPropagation()} + > + + + )} +
+ ); + + return ( + +
+ {comment.body} +
+
+ ); +} diff --git a/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx b/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx index 66ded606..98cead43 100644 --- a/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx +++ b/frontend/src/components/ui-new/containers/ReviewCommentRenderer.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PrimaryButton } from '../primitives/PrimaryButton'; +import { CommentCard } from '../primitives/CommentCard'; import WYSIWYGEditor from '@/components/ui/wysiwyg'; import { useReview, type ReviewComment } from '@/contexts/ReviewProvider'; @@ -41,18 +42,10 @@ export function ReviewCommentRenderer({ if (isEditing) { return ( -
-
- -
+ {t('actions.cancel')} -
-
-
+ + } + > + + ); } return ( -
-
- -
-
+ + + ); } diff --git a/frontend/src/components/ui-new/primitives/CommentCard.tsx b/frontend/src/components/ui-new/primitives/CommentCard.tsx new file mode 100644 index 00000000..52df2939 --- /dev/null +++ b/frontend/src/components/ui-new/primitives/CommentCard.tsx @@ -0,0 +1,51 @@ +import { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; + +export type CommentCardVariant = 'user' | 'github' | 'input'; + +interface CommentCardProps { + /** Determines the visual styling */ + variant: CommentCardVariant; + /** Main content (editor, text, etc.) */ + children: ReactNode; + /** Optional header (author, timestamp) */ + header?: ReactNode; + /** Optional action buttons */ + actions?: ReactNode; + /** Additional className for the outer wrapper */ + className?: string; +} + +const variantStyles: Record = { + user: 'bg-brand/20 border-brand', + github: 'bg-secondary border-border', + input: 'bg-brand/20 border-brand', +}; + +/** + * Shared primitive for displaying comments in diff views. + * Used by ReviewCommentRenderer, GitHubCommentRenderer, and CommentWidgetLine. + */ +export function CommentCard({ + variant, + children, + header, + actions, + className, +}: CommentCardProps) { + return ( +
+
+ {header &&
{header}
} + {children} + {actions &&
{actions}
} +
+
+ ); +} diff --git a/frontend/src/components/ui-new/views/FileTree.tsx b/frontend/src/components/ui-new/views/FileTree.tsx index 0fa89eb3..9cc605a1 100644 --- a/frontend/src/components/ui-new/views/FileTree.tsx +++ b/frontend/src/components/ui-new/views/FileTree.tsx @@ -1,5 +1,7 @@ import { useTranslation } from 'react-i18next'; +import { GithubLogoIcon } from '@phosphor-icons/react'; import { cn } from '@/lib/utils'; +import { Tooltip } from '../primitives/Tooltip'; import { FileTreeSearchBar } from './FileTreeSearchBar'; import { FileTreeNode } from './FileTreeNode'; import type { TreeNode } from '../types/fileTree'; @@ -18,6 +20,14 @@ interface FileTreeProps { isAllExpanded: boolean; onToggleExpandAll: () => void; className?: string; + /** Whether to show GitHub comments */ + showGitHubComments?: boolean; + /** Callback to toggle GitHub comments visibility */ + onToggleGitHubComments?: (show: boolean) => void; + /** Function to get comment count for a file path (handles prefixed paths) */ + getGitHubCommentCountForFile?: (filePath: string) => number; + /** Whether GitHub comments are currently loading */ + isGitHubCommentsLoading?: boolean; } export function FileTree({ @@ -32,6 +42,10 @@ export function FileTree({ isAllExpanded, onToggleExpandAll, className, + showGitHubComments, + onToggleGitHubComments, + getGitHubCommentCountForFile, + isGitHubCommentsLoading, }: FileTreeProps) { const { t } = useTranslation(['tasks', 'common']); @@ -56,6 +70,8 @@ export function FileTree({ ? () => onSelectFile(node.path) : undefined } + commentCount={getGitHubCommentCountForFile?.(node.path)} + showCommentBadge={showGitHubComments} /> {node.type === 'folder' && node.children && @@ -73,12 +89,42 @@ export function FileTree({ contentClassName="flex flex-col flex-1 min-h-0" >
- +
+
+ +
+ {onToggleGitHubComments && ( + + + + )} +
{nodes.length > 0 ? ( diff --git a/frontend/src/components/ui-new/views/FileTreeNode.tsx b/frontend/src/components/ui-new/views/FileTreeNode.tsx index b266aeaf..a65a58d3 100644 --- a/frontend/src/components/ui-new/views/FileTreeNode.tsx +++ b/frontend/src/components/ui-new/views/FileTreeNode.tsx @@ -3,6 +3,7 @@ import { CaretDownIcon, CaretRightIcon, FolderSimpleIcon, + GithubLogoIcon, } from '@phosphor-icons/react'; import { cn } from '@/lib/utils'; import { getFileIcon } from '@/utils/fileTypeIcon'; @@ -17,11 +18,24 @@ interface FileTreeNodeProps { isSelected?: boolean; onToggle?: () => void; onSelect?: () => void; + /** GitHub comment count for this file */ + commentCount?: number; + /** Whether to show the comment badge */ + showCommentBadge?: boolean; } export const FileTreeNode = forwardRef( function FileTreeNode( - { node, depth, isExpanded = false, isSelected = false, onToggle, onSelect }, + { + node, + depth, + isExpanded = false, + isSelected = false, + onToggle, + onSelect, + commentCount, + showCommentBadge, + }, ref ) { const { theme } = useTheme(); @@ -128,6 +142,17 @@ export const FileTreeNode = forwardRef( )} )} + + {/* GitHub comment badge */} + {showCommentBadge && + node.type === 'file' && + commentCount != null && + commentCount > 0 && ( + + + {commentCount} + + )}
); diff --git a/frontend/src/contexts/WorkspaceContext.tsx b/frontend/src/contexts/WorkspaceContext.tsx index 129c0fa2..5117e140 100644 --- a/frontend/src/contexts/WorkspaceContext.tsx +++ b/frontend/src/contexts/WorkspaceContext.tsx @@ -15,13 +15,20 @@ import { import { useAttempt } from '@/hooks/useAttempt'; import { useAttemptRepo } from '@/hooks/useAttemptRepo'; import { useWorkspaceSessions } from '@/hooks/useWorkspaceSessions'; +import { + useGitHubComments, + type NormalizedGitHubComment, +} from '@/hooks/useGitHubComments'; import { attemptsApi } from '@/lib/api'; import type { Workspace as ApiWorkspace, Session, RepoWithTargetBranch, + UnifiedPrComment, } from 'shared/types'; +export type { NormalizedGitHubComment } from '@/hooks/useGitHubComments'; + interface WorkspaceContextValue { workspaceId: string | undefined; /** Real workspace data from API */ @@ -48,6 +55,13 @@ interface WorkspaceContextValue { /** Repos for the current workspace */ repos: RepoWithTargetBranch[]; isReposLoading: boolean; + /** GitHub PR Comments */ + gitHubComments: UnifiedPrComment[]; + isGitHubCommentsLoading: boolean; + showGitHubComments: boolean; + setShowGitHubComments: (show: boolean) => void; + getGitHubCommentsForFile: (filePath: string) => NormalizedGitHubComment[]; + getGitHubCommentCountForFile: (filePath: string) => number; } const WorkspaceContext = createContext(null); @@ -95,6 +109,24 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) { enabled: !isCreateMode, }); + // Get first repo ID for PR comments. + // TODO: Support multiple repos - currently only fetches comments from the primary repo. + const primaryRepoId = repos[0]?.id; + + // GitHub comments hook (fetching, normalization, and helpers) + const { + gitHubComments, + isGitHubCommentsLoading, + showGitHubComments, + setShowGitHubComments, + getGitHubCommentsForFile, + getGitHubCommentCountForFile, + } = useGitHubComments({ + workspaceId, + repoId: primaryRepoId, + enabled: !isCreateMode, + }); + const isLoading = isLoadingList || isLoadingWorkspace; const selectWorkspace = useCallback( @@ -142,6 +174,12 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) { startNewSession, repos, isReposLoading, + gitHubComments, + isGitHubCommentsLoading, + showGitHubComments, + setShowGitHubComments, + getGitHubCommentsForFile, + getGitHubCommentCountForFile, }), [ workspaceId, @@ -162,6 +200,12 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) { startNewSession, repos, isReposLoading, + gitHubComments, + isGitHubCommentsLoading, + showGitHubComments, + setShowGitHubComments, + getGitHubCommentsForFile, + getGitHubCommentCountForFile, ] ); diff --git a/frontend/src/hooks/useGitHubComments.ts b/frontend/src/hooks/useGitHubComments.ts new file mode 100644 index 00000000..3ec942d8 --- /dev/null +++ b/frontend/src/hooks/useGitHubComments.ts @@ -0,0 +1,122 @@ +import { useMemo, useCallback } from 'react'; +import { SplitSide } from '@git-diff-view/react'; +import { usePrComments } from './usePrComments'; +import { + usePersistedExpanded, + PERSIST_KEYS, +} from '@/stores/useUiPreferencesStore'; +import type { UnifiedPrComment } from 'shared/types'; + +/** + * Normalized GitHub comment for diff view display + */ +export interface NormalizedGitHubComment { + id: string; + author: string; + body: string; + createdAt: string; + url: string | null; + filePath: string; + lineNumber: number; + side: SplitSide; + diffHunk: string | null; +} + +interface UseGitHubCommentsOptions { + workspaceId?: string; + repoId?: string; + enabled?: boolean; +} + +interface UseGitHubCommentsResult { + gitHubComments: UnifiedPrComment[]; + isGitHubCommentsLoading: boolean; + showGitHubComments: boolean; + setShowGitHubComments: (show: boolean) => void; + getGitHubCommentsForFile: (filePath: string) => NormalizedGitHubComment[]; + getGitHubCommentCountForFile: (filePath: string) => number; +} + +export function useGitHubComments({ + workspaceId, + repoId, + enabled = true, +}: UseGitHubCommentsOptions): UseGitHubCommentsResult { + // GitHub comments toggle state (persisted) + const [showGitHubComments, setShowGitHubComments] = usePersistedExpanded( + PERSIST_KEYS.showGitHubComments, + true // Default to shown + ); + + // Fetch PR comments for the current workspace + const { data: prCommentsData, isLoading: isGitHubCommentsLoading } = + usePrComments(workspaceId, repoId, { + enabled: enabled && !!repoId, + }); + + const gitHubComments = useMemo( + () => prCommentsData?.comments ?? [], + [prCommentsData?.comments] + ); + + // Normalize GitHub review comments for file matching + const normalizedComments = useMemo(() => { + const normalized: NormalizedGitHubComment[] = []; + for (const comment of gitHubComments) { + if (comment.comment_type !== 'review') continue; + if (comment.line === null) continue; // Skip file-level comments + + normalized.push({ + id: String(comment.id), + author: comment.author, + body: comment.body, + createdAt: comment.created_at, + url: comment.url, + filePath: comment.path, + lineNumber: Number(comment.line), + // Use side from API: "LEFT" = old/deleted side, "RIGHT" = new/added side (default) + side: comment.side === 'LEFT' ? SplitSide.old : SplitSide.new, + diffHunk: comment.diff_hunk, + }); + } + return normalized; + }, [gitHubComments]); + + // Helper to match paths - handles repo prefix in diff paths + // GitHub paths: "frontend/src/file.ts" + // Diff paths: "vibe-kanban/frontend/src/file.ts" (prefixed with repo name) + const pathMatches = useCallback( + (diffPath: string, githubPath: string): boolean => { + return diffPath === githubPath || diffPath.endsWith('/' + githubPath); + }, + [] + ); + + // Get comments for a specific file (handles prefixed paths) + const getGitHubCommentsForFile = useCallback( + (filePath: string): NormalizedGitHubComment[] => { + return normalizedComments.filter((c) => + pathMatches(filePath, c.filePath) + ); + }, + [normalizedComments, pathMatches] + ); + + // Get comment count for a specific file (handles prefixed paths) + const getGitHubCommentCountForFile = useCallback( + (filePath: string): number => { + return normalizedComments.filter((c) => pathMatches(filePath, c.filePath)) + .length; + }, + [normalizedComments, pathMatches] + ); + + return { + gitHubComments, + isGitHubCommentsLoading, + showGitHubComments, + setShowGitHubComments, + getGitHubCommentsForFile, + getGitHubCommentCountForFile, + }; +} diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index 6349d00e..4c61678f 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "Search files...", "noResults": "No matching files", - "title": "Files" + "title": "Files", + "showGitHubComments": "Show GitHub comments", + "hideGitHubComments": "Hide GitHub comments" }, "sections": { "changes": "Changes", diff --git a/frontend/src/i18n/locales/es/common.json b/frontend/src/i18n/locales/es/common.json index c559a9b9..d9b5c22d 100644 --- a/frontend/src/i18n/locales/es/common.json +++ b/frontend/src/i18n/locales/es/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "Buscar archivos...", "noResults": "No hay archivos coincidentes", - "title": "Archivos" + "title": "Archivos", + "showGitHubComments": "Mostrar comentarios de GitHub", + "hideGitHubComments": "Ocultar comentarios de GitHub" }, "sections": { "changes": "Cambios", diff --git a/frontend/src/i18n/locales/ja/common.json b/frontend/src/i18n/locales/ja/common.json index 3932d811..67a790ed 100644 --- a/frontend/src/i18n/locales/ja/common.json +++ b/frontend/src/i18n/locales/ja/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "ファイルを検索...", "noResults": "一致するファイルがありません", - "title": "ファイル" + "title": "ファイル", + "showGitHubComments": "GitHubコメントを表示", + "hideGitHubComments": "GitHubコメントを非表示" }, "sections": { "changes": "変更", diff --git a/frontend/src/i18n/locales/ko/common.json b/frontend/src/i18n/locales/ko/common.json index 18218b40..d5081b0b 100644 --- a/frontend/src/i18n/locales/ko/common.json +++ b/frontend/src/i18n/locales/ko/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "파일 검색...", "noResults": "일치하는 파일 없음", - "title": "파일" + "title": "파일", + "showGitHubComments": "GitHub 댓글 표시", + "hideGitHubComments": "GitHub 댓글 숨기기" }, "sections": { "changes": "변경사항", diff --git a/frontend/src/i18n/locales/zh-Hans/common.json b/frontend/src/i18n/locales/zh-Hans/common.json index f77a740a..3a8c3421 100644 --- a/frontend/src/i18n/locales/zh-Hans/common.json +++ b/frontend/src/i18n/locales/zh-Hans/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "搜索文件...", "noResults": "没有匹配的文件", - "title": "文件" + "title": "文件", + "showGitHubComments": "显示 GitHub 评论", + "hideGitHubComments": "隐藏 GitHub 评论" }, "sections": { "changes": "更改", diff --git a/frontend/src/i18n/locales/zh-Hant/common.json b/frontend/src/i18n/locales/zh-Hant/common.json index 4a2eaadf..b62612a0 100644 --- a/frontend/src/i18n/locales/zh-Hant/common.json +++ b/frontend/src/i18n/locales/zh-Hant/common.json @@ -143,7 +143,9 @@ "fileTree": { "searchPlaceholder": "搜尋檔案...", "noResults": "沒有匹配的檔案", - "title": "檔案" + "title": "檔案", + "showGitHubComments": "顯示 GitHub 評論", + "hideGitHubComments": "隱藏 GitHub 評論" }, "sections": { "changes": "變更", diff --git a/frontend/src/stores/useUiPreferencesStore.ts b/frontend/src/stores/useUiPreferencesStore.ts index 3b2a5e63..7d81a072 100644 --- a/frontend/src/stores/useUiPreferencesStore.ts +++ b/frontend/src/stores/useUiPreferencesStore.ts @@ -30,6 +30,8 @@ export const PERSIST_KEYS = { devServerSection: 'dev-server-section', // Context bar contextBarPosition: 'context-bar-position', + // GitHub comments toggle + showGitHubComments: 'show-github-comments', // Pane sizes sidebarWidth: 'workspaces-sidebar-width', gitPanelWidth: 'workspaces-git-panel-width', @@ -50,6 +52,7 @@ export type PersistKey = | typeof PERSIST_KEYS.processesSection | typeof PERSIST_KEYS.changesSection | typeof PERSIST_KEYS.devServerSection + | typeof PERSIST_KEYS.showGitHubComments | typeof PERSIST_KEYS.sidebarWidth | typeof PERSIST_KEYS.gitPanelWidth | typeof PERSIST_KEYS.changesPanelWidth diff --git a/shared/types.ts b/shared/types.ts index f328fb46..b254a9ef 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -300,7 +300,7 @@ export type GetPrCommentsError = { "type": "no_pr_attached" } | { "type": "cli_n export type GetPrCommentsQuery = { repo_id: string, }; -export type UnifiedPrComment = { "comment_type": "general", id: string, author: string, author_association: string | null, body: string, created_at: string, url: string | null, } | { "comment_type": "review", id: bigint, author: string, author_association: string | null, body: string, created_at: string, url: string | null, path: string, line: bigint | null, diff_hunk: string | null, }; +export type UnifiedPrComment = { "comment_type": "general", id: string, author: string, author_association: string | null, body: string, created_at: string, url: string | null, } | { "comment_type": "review", id: bigint, author: string, author_association: string | null, body: string, created_at: string, url: string | null, path: string, line: bigint | null, side: string | null, diff_hunk: string | null, }; export type ProviderKind = "git_hub" | "azure_dev_ops" | "unknown";