From 49170fe2424ec7d9930820c78b7916a95158ed89 Mon Sep 17 00:00:00 2001 From: Nelson Pires Date: Fri, 20 Feb 2026 11:52:01 -0300 Subject: [PATCH] Restore embedded inline comments in Plan/File/Diff views (#456) * fix(plan-comments): restore embedded inline comment widgets in Plan view Reinstate CodeMirror block-widget comments so plan annotations stay anchored to selected lines and preserve drag/selection behavior without floating overlays. * fix(file-comments): return Files editor comments to embedded widgets Use inline block widgets for file drafts while keeping full-path draft scoping, so similarly named files no longer risk comment collisions. * fix(diff-comments): render inline comments through annotation portals Replace absolute floating positioning with annotation-target portals to keep diff comments attached to their lines across shadow DOM updates. * chore(comments): remove deprecated floating comment hook Drop the unused floating-comment implementation now that plan, file, and diff views all use embedded comment rendering paths. * fix(codemirror): expose gutter width as CSS variable Measure the current CodeMirror gutter and publish --oc-editor-gutter-width on the editor host so inline widgets can size to the visible code area without hardcoded dimensions. * fix(context-panel): publish panel width for embedded widgets Set --oc-context-panel-width on the context panel in both docked and expanded modes so comment widgets can follow the active panel width dynamically. * fix(file-comments): constrain inline input to editor content width Use context-panel and editor-gutter CSS variables to cap comment input width to the visible editor content area, keeping action buttons fully visible in no-wrap mode. * fix(file-comments): constrain inline comment cards to content area Apply the same variable-based width cap to saved comment cards so card actions stay visible when long lines force horizontal scrolling. * fix(diff-comments): stabilize new comment annotation identity Derive new-comment annotation ids from selection side and line range, and reuse that id for portal target lookup and keys to avoid remount glitches. --- .../components/comments/InlineCommentCard.tsx | 2 +- .../comments/InlineCommentInput.tsx | 2 +- .../comments/useFloatingComments.tsx | 198 ------------- .../ui/src/components/layout/ContextPanel.tsx | 19 +- .../ui/src/components/ui/CodeMirrorEditor.tsx | 17 +- .../ui/src/components/views/FilesView.tsx | 81 +++-- .../src/components/views/PierreDiffViewer.tsx | 276 ++++++------------ packages/ui/src/components/views/PlanView.tsx | 238 ++++++++------- 8 files changed, 309 insertions(+), 524 deletions(-) delete mode 100644 packages/ui/src/components/comments/useFloatingComments.tsx diff --git a/packages/ui/src/components/comments/InlineCommentCard.tsx b/packages/ui/src/components/comments/InlineCommentCard.tsx index 4a892abc..f60c510e 100644 --- a/packages/ui/src/components/comments/InlineCommentCard.tsx +++ b/packages/ui/src/components/comments/InlineCommentCard.tsx @@ -36,7 +36,7 @@ export function InlineCommentCard({ return (
lineCount) return undefined; - - const line = view.state.doc.line(lineNumber); - const coords = view.coordsAtPos(line.from); - if (!coords) return undefined; - - const wrapperRect = wrapper.getBoundingClientRect(); - if (position === 'bottom') { - return coords.bottom - wrapperRect.top; - } - return coords.top - wrapperRect.top; -} - -function shouldFlipUp(view: EditorView, endLine: number, scrollContainer: HTMLElement | null): boolean { - const lineCount = view.state.doc.lines; - if (endLine < 1 || endLine > lineCount) return false; - - const line = view.state.doc.line(endLine); - const coords = view.coordsAtPos(line.from); - if (!coords) return false; - - const viewportBottom = scrollContainer - ? scrollContainer.getBoundingClientRect().bottom - : window.innerHeight; - - return (coords.bottom + COMMENT_POPOVER_HEIGHT + 30) > viewportBottom; -} - -function computePosition( - view: EditorView, - wrapper: HTMLElement, - scrollContainer: HTMLElement | null, - range: { start: number; end: number }, -): CommentPos | undefined { - const flipUp = shouldFlipUp(view, range.end, scrollContainer); - - const top = flipUp - ? getLineTop(view, wrapper, range.start, 'top') - : getLineTop(view, wrapper, range.end, 'bottom'); - - if (top === undefined) return undefined; - return { top, flipUp }; -} - -type FloatingCommentsProps = { - editorView: EditorView | null; - wrapperRef: React.RefObject; - fileDrafts: InlineCommentDraft[]; - editingDraftId: string | null; - commentText: string; - lineSelection: SelectedLineRange | null; - isDragging: boolean; - fileLabel: string; - onSaveComment: (text: string, range?: SelectedLineRange) => void; - onCancelComment: () => void; - onEditDraft: (draft: InlineCommentDraft) => void; - onDeleteDraft: (draft: InlineCommentDraft) => void; -}; - -export function useFloatingComments({ - editorView, - wrapperRef, - fileDrafts, - editingDraftId, - commentText, - lineSelection, - isDragging, - fileLabel, - onSaveComment, - onCancelComment, - onEditDraft, - onDeleteDraft, -}: FloatingCommentsProps): React.ReactNode { - const [positions, setPositions] = React.useState>({}); - - const updatePositions = React.useCallback(() => { - const view = editorView; - const wrapper = wrapperRef.current; - if (!view || !wrapper) return; - - const scrollContainer = wrapper.closest('.overlay-scrollbar-container') as HTMLElement | null; - const next: Record = {}; - - for (const d of fileDrafts) { - next[d.id] = computePosition(view, wrapper, scrollContainer, { - start: d.startLine, - end: d.endLine, - }); - } - - if (lineSelection && !editingDraftId && !isDragging) { - next['__new__'] = computePosition(view, wrapper, scrollContainer, { - start: lineSelection.start, - end: lineSelection.end, - }); - } - - setPositions(next); - }, [editorView, wrapperRef, fileDrafts, editingDraftId, lineSelection, isDragging]); - - React.useEffect(() => { - requestAnimationFrame(updatePositions); - }, [updatePositions]); - - // Also update on scroll - React.useEffect(() => { - const wrapper = wrapperRef.current; - if (!wrapper) return; - - const scrollContainer = wrapper.closest('.overlay-scrollbar-container') as HTMLElement | null; - if (!scrollContainer) return; - - const onScroll = () => requestAnimationFrame(updatePositions); - scrollContainer.addEventListener('scroll', onScroll, { passive: true }); - return () => scrollContainer.removeEventListener('scroll', onScroll); - }, [wrapperRef, updatePositions]); - - const popoverStyle = (flipUp: boolean): React.CSSProperties => flipUp - ? { position: 'absolute', bottom: 'calc(100% + 4px)', right: -8, zIndex: 40, width: 380, maxWidth: 'min(380px, calc(100vw - 48px))', borderRadius: 14 } - : { position: 'absolute', top: 'calc(100% + 4px)', right: -8, zIndex: 40, width: 380, maxWidth: 'min(380px, calc(100vw - 48px))', borderRadius: 14 }; - - return ( - <> - {fileDrafts.map((d) => { - const pos = positions[d.id]; - if (!pos) return null; - - if (d.id === editingDraftId) { - return ( -
-
- -
-
- ); - } - - return ( -
- onEditDraft(d)} - onDelete={() => onDeleteDraft(d)} - /> -
- ); - })} - - {lineSelection && !editingDraftId && !isDragging && positions['__new__'] && ( -
-
- -
-
- )} - - ); -} diff --git a/packages/ui/src/components/layout/ContextPanel.tsx b/packages/ui/src/components/layout/ContextPanel.tsx index 93cbf102..8eb8980f 100644 --- a/packages/ui/src/components/layout/ContextPanel.tsx +++ b/packages/ui/src/components/layout/ContextPanel.tsx @@ -194,6 +194,17 @@ export const ContextPanel: React.FC = () => { return null; } + const panelStyle: React.CSSProperties = isExpanded + ? { + ['--oc-context-panel-width' as string]: '100vw', + } + : { + width: `${width}px`, + minWidth: `${width}px`, + maxWidth: `${width}px`, + ['--oc-context-panel-width' as string]: `${width}px`, + }; + return (