| ... | @@ -1779,6 +1779,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1779,6 +1779,8 @@ export default function ChatView(props: ChatViewProps) { |
| 1779 | const [composerTimelineInset, setComposerTimelineInset] = useState(0); | 1779 | const [composerTimelineInset, setComposerTimelineInset] = useState(0); |
| 1780 | const composerTimelineInsetRef = useRef(0); | 1780 | const composerTimelineInsetRef = useRef(0); |
| 1781 | const composerRestingRef = useRef(false); | 1781 | const composerRestingRef = useRef(false); |
| | 1782 | // The last overlay height the composer published for its settled layout. |
| | 1783 | const composerOverlayHeightRef = useRef(0); |
| 1782 | const [scrollToEndClearance, setScrollToEndClearance] = useState(0); | 1784 | const [scrollToEndClearance, setScrollToEndClearance] = useState(0); |
| 1783 | const isAtEndRef = useRef(true); | 1785 | const isAtEndRef = useRef(true); |
| 1784 | const isTimelineAtLogicalEnd = useCallback( | 1786 | const isTimelineAtLogicalEnd = useCallback( |
| ... | @@ -5868,19 +5870,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -5868,19 +5870,8 @@ export default function ChatView(props: ChatViewProps) { |
| 5868 | ? activePlan.steps | 5870 | ? activePlan.steps |
| 5869 | : null; | 5871 | : null; |
| 5870 | | 5872 | |
| 5871 | const publishComposerOverlayHeight = useCallback( | 5873 | const publishScrollToEndClearance = useCallback( |
| 5872 | (height: number) => { | 5874 | (overlayHeight: 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 | } | | |
| 5884 | const mainSurface = composerOverlayElement?.querySelector<HTMLElement>( | 5875 | const mainSurface = composerOverlayElement?.querySelector<HTMLElement>( |
| 5885 | '[data-chat-composer-main-surface="true"]', | 5876 | '[data-chat-composer-main-surface="true"]', |
| 5886 | ); | 5877 | ); |
| ... | @@ -5890,7 +5881,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -5890,7 +5881,7 @@ export default function ChatView(props: ChatViewProps) { |
| 5890 | const clearance = | 5881 | const clearance = |
| 5891 | composerOverlayElement && mainSurface && button | 5882 | composerOverlayElement && mainSurface && button |
| 5892 | ? resolveScrollToEndClearance({ | 5883 | ? resolveScrollToEndClearance({ |
| 5893 | overlayHeight: nextHeight, | 5884 | overlayHeight, |
| 5894 | mainSurfaceTop: mainSurface.getBoundingClientRect().top, | 5885 | mainSurfaceTop: mainSurface.getBoundingClientRect().top, |
| 5895 | button: button.getBoundingClientRect(), | 5886 | button: button.getBoundingClientRect(), |
| 5896 | attachments: Array.from( | 5887 | attachments: Array.from( |
| ... | @@ -5900,11 +5891,29 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -5900,11 +5891,29 @@ export default function ChatView(props: ChatViewProps) { |
| 5900 | (element) => element.getBoundingClientRect(), | 5891 | (element) => element.getBoundingClientRect(), |
| 5901 | ), | 5892 | ), |
| 5902 | }) | 5893 | }) |
| 5903 | : nextHeight; | 5894 | : overlayHeight; |
| 5904 | setScrollToEndClearance(clearance); | 5895 | setScrollToEndClearance(clearance); |
| 5905 | }, | 5896 | }, |
| 5906 | [composerOverlayElement], | 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 | // The composer reports its resting flag from a layout effect, which runs | 5917 | // The composer reports its resting flag from a layout effect, which runs |
| 5909 | // before this component's own layout effects and before any resize | 5918 | // before this component's own layout effects and before any resize |
| 5910 | // observation, so every measurement below sees the flag for its layout. | 5919 | // observation, so every measurement below sees the flag for its layout. |
| ... | @@ -5937,7 +5946,17 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -5937,7 +5946,17 @@ export default function ChatView(props: ChatViewProps) { |
| 5937 | return () => { | 5946 | return () => { |
| 5938 | resizeObserver.disconnect(); | 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 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); | 5960 | const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); |
| 5942 | const activeThreadReferenceCopyTarget = useMemo( | 5961 | const activeThreadReferenceCopyTarget = useMemo( |
| 5943 | () => | 5962 | () => |