From 1e012d37b447c8c295da7b4b6ee0c035b048e7a7 Mon Sep 17 00:00:00 2001 From: Nelson Pires Date: Sun, 8 Feb 2026 10:42:50 -0300 Subject: [PATCH] Opencode/daring jackal (#356) * feat: export inline comment components Expose InlineCommentCard and InlineCommentInput from the comments index Enable importing both components from a single module path * feat: add InlineCommentCard for inline comments with collapse Enable collapsing for long comments with Show more and Show less Provide dropdown menu with edit and delete actions Show file label and line range in the header * feat: add InlineCommentInput component Add InlineCommentInput component for per-line comments Enable Cmd/Ctrl+Enter to save and Escape to cancel Show file label and line range when provided and focus input on mount * feat: render block widgets in CodeMirrorEditor Add blockWidgets prop to configure widgets after lines Render React portals into per-widget containers within the editor Introduce BlockWidget and DOM container mapping to host widgets * feat(PierreDiffViewer): enable inline comment drafting and theming Add inline comment draft management for diff lines Render InlineCommentInput and InlineCommentCard for comments in diff Integrate theme system to support dark and light modes * feat: improve inline comment editing in PlanView Enable auto-scroll for mobile when editing inline comments Cancel edits when clicking outside comment widgets or code gutter Extend save handler to accept explicit range and guard against empty text * feat(pierre-diff-viewer): enable mobile tap-to-extend selection Extend selection on mobile when tapping the same side to include range Show saved annotation cards on all devices * fix(plan): show saved cards on all devices Render saved cards on all devices Ensure saved cards appear for mobile layouts as well * refactor: simplify state usage and remove focus hacks Remove pending focus state and related effects in PlanView and PierreDiffViewer Initialize UI store usage via useUIStore() without destructuring values in PlanView and PierreDiffViewer Align annotation handling to DiffLineAnnotation metadata and include side in lineRange for inline comments --- .../components/comments/InlineCommentCard.tsx | 116 +++ .../comments/InlineCommentInput.tsx | 138 ++++ packages/ui/src/components/comments/index.ts | 2 + .../ui/src/components/ui/CodeMirrorEditor.tsx | 128 ++- .../src/components/views/PierreDiffViewer.tsx | 739 +++++++----------- packages/ui/src/components/views/PlanView.tsx | 341 +++----- 6 files changed, 775 insertions(+), 689 deletions(-) create mode 100644 packages/ui/src/components/comments/InlineCommentCard.tsx create mode 100644 packages/ui/src/components/comments/InlineCommentInput.tsx create mode 100644 packages/ui/src/components/comments/index.ts diff --git a/packages/ui/src/components/comments/InlineCommentCard.tsx b/packages/ui/src/components/comments/InlineCommentCard.tsx new file mode 100644 index 00000000..4a892abc --- /dev/null +++ b/packages/ui/src/components/comments/InlineCommentCard.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { RiMoreLine, RiDeleteBinLine, RiEditLine, RiArrowDownSLine, RiArrowUpSLine } from '@remixicon/react'; +import type { InlineCommentDraft } from '@/stores/useInlineCommentDraftStore'; +import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; + +interface InlineCommentCardProps { + draft: InlineCommentDraft; + onEdit: () => void; + onDelete: () => void; + className?: string; +} + +export function InlineCommentCard({ + draft, + onEdit, + onDelete, + className, +}: InlineCommentCardProps) { + const themeContext = useOptionalThemeSystem(); + const currentTheme = themeContext?.currentTheme; + const [isOpen, setIsOpen] = useState(false); + + // Check if content is long enough to warrant collapsing (rough estimate) + // In a real app we might measure line height, but length check is a good proxy for now + const isLongContent = draft.text.length > 150 || draft.text.split('\n').length > 3; + + return ( +
+
+
+
+ + {draft.fileLabel} + + + Lines {draft.startLine}-{draft.endLine} + {draft.side && ({draft.side})} +
+ + +
+ {draft.text} +
+ + {isLongContent && ( + + + + )} + + + {/* Used for animation purposes if we want to animate height */} + +
+
+ + + + + + + + + Edit comment + + + + Delete comment + + + +
+
+ ); +} diff --git a/packages/ui/src/components/comments/InlineCommentInput.tsx b/packages/ui/src/components/comments/InlineCommentInput.tsx new file mode 100644 index 00000000..995f011e --- /dev/null +++ b/packages/ui/src/components/comments/InlineCommentInput.tsx @@ -0,0 +1,138 @@ +import React, { useRef, useEffect } from 'react'; +import { useOptionalThemeSystem } from '@/contexts/useThemeSystem'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import { useDeviceInfo } from '@/lib/device'; + +export interface InlineCommentInputProps { + initialText?: string; + onSave: (text: string, range?: { start: number; end: number; side?: 'additions' | 'deletions' }) => void; + onCancel: () => void; + fileLabel?: string; + lineRange?: { start: number; end: number; side?: 'additions' | 'deletions' }; + isEditing?: boolean; + className?: string; +} + +export function InlineCommentInput({ + initialText = '', + onSave, + onCancel, + fileLabel, + lineRange, + isEditing = false, + className, +}: InlineCommentInputProps) { + const themeContext = useOptionalThemeSystem(); + const currentTheme = themeContext?.currentTheme; + const { isMobile } = useDeviceInfo(); + const [text, setText] = React.useState(initialText); + const textareaRef = useRef(null); + + // Stable range snapshot to prevent race with selection clearing + const stableRangeRef = useRef(lineRange); + useEffect(() => { + if (lineRange) { + stableRangeRef.current = lineRange; + } + }, [lineRange]); + + // Focus on mount (desktop only) or when becoming visible + useEffect(() => { + if (!isMobile && textareaRef.current) { + textareaRef.current.focus(); + // Move cursor to end + const len = textareaRef.current.value.length; + textareaRef.current.setSelectionRange(len, len); + } else if (isMobile && textareaRef.current) { + // Scroll into view on mobile + textareaRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [isMobile]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { + e.preventDefault(); + if (text.trim()) { + onSave(text, stableRangeRef.current); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + onCancel(); + } + }; + + const handleSaveClick = (e: React.MouseEvent | React.TouchEvent | React.PointerEvent) => { + // Stop propagation to prevent parent selection clearing before save + e.stopPropagation(); + if (text.trim()) { + onSave(text, stableRangeRef.current); + } + }; + + return ( +
e.stopPropagation()} + onTouchStart={(e) => e.stopPropagation()} + > +
+ {(fileLabel || lineRange) && ( +
+ {fileLabel && {fileLabel}} + {fileLabel && lineRange && } + {lineRange && Lines {lineRange.start}-{lineRange.end}} +
+ )} + +