| ... | ... | @@ -1779,6 +1779,8 @@ export default function ChatView(props: ChatViewProps) { |
| 1779 | 1779 | const [composerTimelineInset, setComposerTimelineInset] = useState(0); |
| 1780 | 1780 | const composerTimelineInsetRef = useRef(0); |
| 1781 | 1781 | const composerRestingRef = useRef(false); |
| 1782 | // The last overlay height the composer published for its settled layout. |
| 1783 | const composerOverlayHeightRef = useRef(0); |
| 1782 | 1784 | const [scrollToEndClearance, setScrollToEndClearance] = useState(0); |
| 1783 | 1785 | const isAtEndRef = useRef(true); |
| 1784 | 1786 | const isTimelineAtLogicalEnd = useCallback( |
| ... | ... | @@ -5868,19 +5870,8 @@ export default function ChatView(props: ChatViewProps) { |
| 5868 | 5870 | ? activePlan.steps |
| 5869 | 5871 | : null; |
| 5870 | 5872 | |
| 5871 | | const publishComposerOverlayHeight = useCallback( |
| 5872 | | (height: number) => { |
| 5873 | | const nextHeight = Math.ceil(height); |
| 5874 | | if (nextHeight <= 0) return; |
| 5875 | | const nextInset = resolveComposerTimelineInset({ |
| 5876 | | currentInset: composerTimelineInsetRef.current, |
| 5877 | | overlayHeight: nextHeight, |
| 5878 | | isResting: composerRestingRef.current, |
| 5879 | | }); |
| 5880 | | if (composerTimelineInsetRef.current !== nextInset) { |
| 5881 | | composerTimelineInsetRef.current = nextInset; |
| 5882 | | setComposerTimelineInset(nextInset); |
| 5883 | | } |
| 5873 | const publishScrollToEndClearance = useCallback( |
| 5874 | (overlayHeight: number) => { |
| 5884 | 5875 | const mainSurface = composerOverlayElement?.querySelector<HTMLElement>( |
| 5885 | 5876 | '[data-chat-composer-main-surface="true"]', |
| 5886 | 5877 | ); |
| ... | ... | @@ -5890,7 +5881,7 @@ export default function ChatView(props: ChatViewProps) { |
| 5890 | 5881 | const clearance = |
| 5891 | 5882 | composerOverlayElement && mainSurface && button |
| 5892 | 5883 | ? resolveScrollToEndClearance({ |
| 5893 | | overlayHeight: nextHeight, |
| 5884 | overlayHeight, |
| 5894 | 5885 | mainSurfaceTop: mainSurface.getBoundingClientRect().top, |
| 5895 | 5886 | button: button.getBoundingClientRect(), |
| 5896 | 5887 | attachments: Array.from( |
| ... | ... | @@ -5900,11 +5891,29 @@ export default function ChatView(props: ChatViewProps) { |
| 5900 | 5891 | (element) => element.getBoundingClientRect(), |
| 5901 | 5892 | ), |
| 5902 | 5893 | }) |
| 5903 | | : nextHeight; |
| 5894 | : overlayHeight; |
| 5904 | 5895 | setScrollToEndClearance(clearance); |
| 5905 | 5896 | }, |
| 5906 | 5897 | [composerOverlayElement], |
| 5907 | 5898 | ); |
| 5899 | const publishComposerOverlayHeight = useCallback( |
| 5900 | (height: number) => { |
| 5901 | const nextHeight = Math.ceil(height); |
| 5902 | if (nextHeight <= 0) return; |
| 5903 | composerOverlayHeightRef.current = nextHeight; |
| 5904 | const nextInset = resolveComposerTimelineInset({ |
| 5905 | currentInset: composerTimelineInsetRef.current, |
| 5906 | overlayHeight: nextHeight, |
| 5907 | isResting: composerRestingRef.current, |
| 5908 | }); |
| 5909 | if (composerTimelineInsetRef.current !== nextInset) { |
| 5910 | composerTimelineInsetRef.current = nextInset; |
| 5911 | setComposerTimelineInset(nextInset); |
| 5912 | } |
| 5913 | publishScrollToEndClearance(nextHeight); |
| 5914 | }, |
| 5915 | [publishScrollToEndClearance], |
| 5916 | ); |
| 5908 | 5917 | // The composer reports its resting flag from a layout effect, which runs |
| 5909 | 5918 | // before this component's own layout effects and before any resize |
| 5910 | 5919 | // observation, so every measurement below sees the flag for its layout. |
| ... | ... | @@ -5937,7 +5946,17 @@ export default function ChatView(props: ChatViewProps) { |
| 5937 | 5946 | return () => { |
| 5938 | 5947 | resizeObserver.disconnect(); |
| 5939 | 5948 | }; |
| 5940 | | }, [composerOverlayElement, publishComposerOverlayHeight, showScrollToBottom]); |
| 5949 | }, [composerOverlayElement, publishComposerOverlayHeight]); |
| 5950 | // The pill mounts and unmounts in the same commits that expand or rest the |
| 5951 | // composer, and a fast fling lands there while the previous resting tween |
| 5952 | // still pins the overlay at its old height. Measuring the overlay here would |
| 5953 | // publish that stale height against the new resting flag, drop the timeline |
| 5954 | // reservation, and yank the scroll position. The pill only needs its |
| 5955 | // clearance, so it reuses the height the composer last published. |
| 5956 | useLayoutEffect(() => { |
| 5957 | if (!composerOverlayElement) return; |
| 5958 | publishScrollToEndClearance(composerOverlayHeightRef.current); |
| 5959 | }, [composerOverlayElement, publishScrollToEndClearance, showScrollToBottom]); |
| 5941 | 5960 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); |
| 5942 | 5961 | const activeThreadReferenceCopyTarget = useMemo( |
| 5943 | 5962 | () => |