From ae7283e021a58c2717005a5745def6e4f0afa2aa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 15:39:37 -0800 Subject: [PATCH] style --- apps/web/src/components/ChatView.tsx | 65 ++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 55d34424aad4a85eec434f35746bdc301bbf5dcb..e783d379ef0bcb8f2c7beee2f5b4287ecf8d97ca 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -637,6 +637,11 @@ export default function ChatView({ threadId }: ChatViewProps) { const lastTouchClientYRef = useRef(null); const pendingUserScrollUpIntentRef = useRef(false); const pendingAutoScrollFrameRef = useRef(null); + const pendingInteractionAnchorRef = useRef<{ + element: HTMLElement; + top: number; + } | null>(null); + const pendingInteractionAnchorFrameRef = useRef(null); const composerEditorRef = useRef(null); const composerFormRef = useRef(null); const composerFormHeightRef = useRef(0); @@ -1055,7 +1060,8 @@ export default function ChatView({ threadId }: ChatViewProps) { return [...serverMessagesWithPreviewHandoff, ...pendingMessages]; }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]); const timelineEntries = useMemo( - () => deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries), + () => + deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries), [activeThread?.proposedPlans, timelineMessages, workLogEntries], ); const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = @@ -1675,6 +1681,12 @@ export default function ChatView({ threadId }: ChatViewProps) { pendingAutoScrollFrameRef.current = null; window.cancelAnimationFrame(pendingFrame); }, []); + const cancelPendingInteractionAnchorAdjustment = useCallback(() => { + const pendingFrame = pendingInteractionAnchorFrameRef.current; + if (pendingFrame === null) return; + pendingInteractionAnchorFrameRef.current = null; + window.cancelAnimationFrame(pendingFrame); + }, []); const scheduleStickToBottom = useCallback(() => { if (pendingAutoScrollFrameRef.current !== null) return; pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => { @@ -1682,6 +1694,40 @@ export default function ChatView({ threadId }: ChatViewProps) { scrollMessagesToBottom(); }); }, [scrollMessagesToBottom]); + const onMessagesClickCapture = useCallback( + (event: React.MouseEvent) => { + const scrollContainer = messagesScrollRef.current; + if (!scrollContainer || !(event.target instanceof Element)) return; + + const trigger = event.target.closest( + "button, summary, [role='button'], [data-scroll-anchor-target]", + ); + if (!trigger || !scrollContainer.contains(trigger)) return; + + pendingInteractionAnchorRef.current = { + element: trigger, + top: trigger.getBoundingClientRect().top, + }; + + cancelPendingInteractionAnchorAdjustment(); + pendingInteractionAnchorFrameRef.current = window.requestAnimationFrame(() => { + pendingInteractionAnchorFrameRef.current = null; + const anchor = pendingInteractionAnchorRef.current; + pendingInteractionAnchorRef.current = null; + const activeScrollContainer = messagesScrollRef.current; + if (!anchor || !activeScrollContainer) return; + if (!anchor.element.isConnected || !activeScrollContainer.contains(anchor.element)) return; + + const nextTop = anchor.element.getBoundingClientRect().top; + const delta = nextTop - anchor.top; + if (Math.abs(delta) < 0.5) return; + + activeScrollContainer.scrollTop += delta; + lastKnownScrollTopRef.current = activeScrollContainer.scrollTop; + }); + }, + [cancelPendingInteractionAnchorAdjustment], + ); const forceStickToBottom = useCallback(() => { cancelPendingStickToBottom(); scrollMessagesToBottom(); @@ -1751,8 +1797,9 @@ export default function ChatView({ threadId }: ChatViewProps) { useEffect(() => { return () => { cancelPendingStickToBottom(); + cancelPendingInteractionAnchorAdjustment(); }; - }, [cancelPendingStickToBottom]); + }, [cancelPendingInteractionAnchorAdjustment, cancelPendingStickToBottom]); useLayoutEffect(() => { if (!activeThread?.id) return; shouldAutoScrollRef.current = true; @@ -2279,7 +2326,7 @@ export default function ChatView({ threadId }: ChatViewProps) { if (showPlanFollowUpPrompt && activeProposedPlan) { const followUpText = trimmed.length > 0 - ? trimmed + ? trimmed : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown); const nextInteractionMode = trimmed.length > 0 ? "plan" : "default"; promptRef.current = ""; @@ -3323,6 +3370,7 @@ export default function ChatView({ threadId }: ChatViewProps) { ref={setMessagesScrollContainerRef} className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4" onScroll={onMessagesScroll} + onClickCapture={onMessagesClickCapture} onWheel={onMessagesWheel} onPointerDown={onMessagesPointerDown} onPointerUp={onMessagesPointerUp} @@ -4270,15 +4318,14 @@ const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({ return (
- Plan ready + Plan ready {planTitle ? ( - {planTitle} + {planTitle} ) : null}
-
- Leave the composer empty to implement here, use the menu to implement in a new thread, or - type feedback to refine it. -
+ {/*
+ Review the plan +
*/}
); }); -- 2.54.0