diff --git a/web/src/App.tsx b/web/src/App.tsx index 8942137..2e6e907 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -452,6 +452,7 @@ export default function App() { const searchRunCounterRef = useRef(0); const shouldAutoScrollRef = useRef(true); const wasSendingRef = useRef(false); + const pendingReplyScrollRef = useRef(false); const [contextMenu, setContextMenu] = useState(null); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const [sidebarQuery, setSidebarQuery] = useState(""); @@ -643,6 +644,12 @@ export default function App() { }, [providerModelPreferences]); const selectedKey = selectedItem ? `${selectedItem.kind}:${selectedItem.id}` : null; + const isChatReplyStreamingInView = + isSending && + draftKind !== "search" && + selectedItem?.kind !== "search" && + !!pendingChatState && + (!pendingChatState.chatId || (selectedItem?.kind === "chat" && selectedItem.id === pendingChatState.chatId)); useEffect(() => { shouldAutoScrollRef.current = true; @@ -675,11 +682,27 @@ export default function App() { if (draftKind === "search" || selectedItem?.kind === "search") return; const wasSending = wasSendingRef.current; wasSendingRef.current = isSending; - if (wasSending && !isSending) return; + if (isSending) return; + if (wasSending) { + shouldAutoScrollRef.current = false; + return; + } if (!shouldAutoScrollRef.current) return; - transcriptEndRef.current?.scrollIntoView({ behavior: isSending ? "smooth" : "auto", block: "end" }); + transcriptEndRef.current?.scrollIntoView({ behavior: "auto", block: "end" }); }, [draftKind, selectedChat?.messages.length, isSending, selectedItem?.kind, selectedKey]); + useEffect(() => { + if (!isChatReplyStreamingInView || !pendingReplyScrollRef.current) return; + pendingReplyScrollRef.current = false; + shouldAutoScrollRef.current = true; + + window.requestAnimationFrame(() => { + const container = transcriptContainerRef.current; + if (!container) return; + container.scrollTo({ top: container.scrollHeight, behavior: "smooth" }); + }); + }, [isChatReplyStreamingInView, pendingChatState?.chatId]); + useEffect(() => { if (isSending) return; const hasWorkspaceSelection = Boolean(selectedItem) || draftKind !== null; @@ -697,13 +720,7 @@ export default function App() { const messages = selectedChat?.messages ?? []; const isSearchMode = draftKind ? draftKind === "search" : selectedItem?.kind === "search"; const isSearchRunning = isSending && isSearchMode; - const isSendingActiveChat = - isSending && - !isSearchMode && - !!pendingChatState && - !!pendingChatState.chatId && - selectedItem?.kind === "chat" && - selectedItem.id === pendingChatState.chatId; + const isSendingActiveChat = isChatReplyStreamingInView; const displayMessages = useMemo(() => { if (!pendingChatState) return messages.filter(isDisplayableMessage); if (pendingChatState.chatId) { @@ -837,6 +854,8 @@ export default function App() { }, [contextMenu]); const handleSendChat = async (content: string) => { + pendingReplyScrollRef.current = true; + const optimisticUserMessage: Message = { id: `temp-user-${Date.now()}`, createdAt: new Date().toISOString(), @@ -1424,7 +1443,7 @@ export default function App() {
{ const container = transcriptContainerRef.current; if (!container) return; @@ -1443,6 +1462,9 @@ export default function App() { onStartChat={selectedSearch ? handleStartChatFromSearch : undefined} /> )} + {isChatReplyStreamingInView ? ( +