diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx index 5fa69af7a71..6975b4c44cb 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx @@ -122,9 +122,26 @@ describe('PRCommentsList', () => { }) ) - expect(markup.indexOf('Existing review context')).toBeLessThan(markup.indexOf('Add a comment…')) + expect(markup.indexOf('Existing review context')).toBeLessThan( + markup.indexOf('Add a comment...') + ) expect(markup).not.toContain('Add a PR comment') }) + + it('uses the collapsed composer as the empty comments state', () => { + const markup = renderToStaticMarkup( + React.createElement(PRCommentsList, { + comments: [], + commentsLoading: false, + onAddComment: () => Promise.resolve({ ok: true as const }) + }) + ) + + expect(markup).toContain('Start conversation...') + expect(markup).not.toContain('No comments yet') + expect(markup).not.toContain('Add a comment') + expect((markup.match(/lucide-message-square/g) ?? []).length).toBe(1) + }) }) describe('getFailedChecksForDetails', () => { diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index ff08a66778d..73708c9baee 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -1457,6 +1457,47 @@ function ResolvedCommentGroupAccordion({ ) } +function findVerticalScrollParent(element: HTMLElement): HTMLElement | null { + let parent = element.parentElement + while (parent) { + const style = window.getComputedStyle(parent) + const canScroll = style.overflowY === 'auto' || style.overflowY === 'scroll' + if (canScroll && parent.scrollHeight > parent.clientHeight) { + return parent + } + parent = parent.parentElement + } + return null +} + +function scrollElementBottomIntoView(element: HTMLElement): void { + const scrollParent = findVerticalScrollParent(element) + if (!scrollParent) { + element.scrollIntoView({ block: 'end', behavior: 'smooth' }) + return + } + + const padding = 8 + const parentRect = scrollParent.getBoundingClientRect() + const elementRect = element.getBoundingClientRect() + const bottomOverflow = elementRect.bottom - parentRect.bottom + padding + if (bottomOverflow > 0) { + scrollParent.scrollTo({ + top: scrollParent.scrollTop + bottomOverflow, + behavior: 'smooth' + }) + return + } + + const topOverflow = elementRect.top - parentRect.top - padding + if (topOverflow < 0) { + scrollParent.scrollTo({ + top: Math.max(0, scrollParent.scrollTop + topOverflow), + behavior: 'smooth' + }) + } +} + /** Renders the PR comments section below checks. */ export function PRCommentsList({ comments, @@ -1478,12 +1519,81 @@ export function PRCommentsList({ const [commentFilter, setCommentFilter] = useState('all') const [replyingGroupId, setReplyingGroupId] = useState(null) const [isAddingComment, setIsAddingComment] = useState(false) + const addCommentSurfaceRef = useRef(null) + const shouldScrollAddCommentRef = useRef(false) const commentCounts = React.useMemo(() => getPRCommentAudienceCounts(comments), [comments]) const visibleComments = React.useMemo( () => filterPRCommentsByAudience(comments, commentFilter), [commentFilter, comments] ) const groups = React.useMemo(() => groupPRComments(visibleComments), [visibleComments]) + useEffect(() => { + if (!isAddingComment || !shouldScrollAddCommentRef.current) { + return + } + shouldScrollAddCommentRef.current = false + let secondFrame: number | null = null + const scrollComposerIntoView = (): void => { + const surface = addCommentSurfaceRef.current + if (surface) { + scrollElementBottomIntoView(surface) + } + } + const firstFrame = window.requestAnimationFrame(() => { + secondFrame = window.requestAnimationFrame(scrollComposerIntoView) + }) + // Why: the composer expands and focuses in separate layout passes; the + // timeout catches the final height so the footer is visible in short panels. + const settledTimer = window.setTimeout(scrollComposerIntoView, 120) + return () => { + window.cancelAnimationFrame(firstFrame) + if (secondFrame !== null) { + window.cancelAnimationFrame(secondFrame) + } + window.clearTimeout(settledTimer) + } + }, [isAddingComment]) + + const startAddComment = useCallback(() => { + shouldScrollAddCommentRef.current = true + setIsAddingComment(true) + }, []) + + const cancelAddComment = useCallback(() => { + shouldScrollAddCommentRef.current = false + setIsAddingComment(false) + }, []) + + const renderAddCommentSurface = (empty: boolean): React.JSX.Element => ( +
+ {isAddingComment ? ( + ({ ok: false, error: 'Commenting unavailable.' }))} + /> + ) : ( + // Why: the empty comments state should be a single composer affordance; + // duplicating "no comments" copy or the header icon makes the panel noisy. + + )} +
+ ) return (
@@ -1530,11 +1640,11 @@ export function PRCommentsList({
+ ) : comments.length === 0 && onAddComment ? ( + renderAddCommentSurface(true) ) : comments.length === 0 ? ( - // Why: with the composer pinned below as the call to action, the empty - // state stays a quiet caption instead of competing for attention.
- {onAddComment ? 'No comments yet — start the conversation below.' : 'No comments'} + No comments
) : visibleComments.length === 0 ? (
@@ -1574,34 +1684,7 @@ export function PRCommentsList({ })}
)} - {onAddComment && ( -
- {isAddingComment ? ( - setIsAddingComment(false)} - onSubmit={onAddComment} - /> - ) : ( - // Quiet full-width affordance that reads as a composer field, not a - // lonely button; clicking expands the markdown editor in place. - - )} -
- )} + {onAddComment && comments.length > 0 && renderAddCommentSurface(false)}
) } diff --git a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx index 2db88e5b0d7..b51b7ac3023 100644 --- a/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx +++ b/src/renderer/src/components/right-sidebar/right-panel-comment-composer.tsx @@ -81,7 +81,6 @@ export function RightPanelCommentComposer({ const autoFocusTimerRef = useRef | null>(null) const selectionTimerRef = useRef | null>(null) const isMac = navigator.userAgent.includes('Mac') - const modLabel = isMac ? '⌘' : 'Ctrl' useEffect(() => { const textarea = textareaRef.current @@ -176,7 +175,10 @@ export function RightPanelCommentComposer({ return (
@@ -219,39 +221,43 @@ export function RightPanelCommentComposer({ {error}
)} -
- -
- {onCancel && ( +
+ {onCancel && ( + + )} + + - )} - -
+ +
)