| ... | @@ -45,6 +45,7 @@ import { | ... | @@ -45,6 +45,7 @@ import { |
| 45 | formatElapsed, | 45 | formatElapsed, |
| 46 | formatTimestamp, | 46 | formatTimestamp, |
| 47 | } from "../session-logic"; | 47 | } from "../session-logic"; |
| | 48 | import { isScrollContainerNearBottom } from "../chat-scroll"; |
| 48 | import { useStore } from "../store"; | 49 | import { useStore } from "../store"; |
| 49 | import type { ChatImageAttachment } from "../types"; | 50 | import type { ChatImageAttachment } from "../types"; |
| 50 | import BranchToolbar from "./BranchToolbar"; | 51 | import BranchToolbar from "./BranchToolbar"; |
| ... | @@ -145,6 +146,7 @@ export default function ChatView() { | ... | @@ -145,6 +146,7 @@ export default function ChatView() { |
| 145 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | 146 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 146 | const messagesScrollRef = useRef<HTMLDivElement>(null); | 147 | const messagesScrollRef = useRef<HTMLDivElement>(null); |
| 147 | const messagesEndRef = useRef<HTMLDivElement>(null); | 148 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | 149 | const shouldAutoScrollRef = useRef(true); |
| 148 | const textareaRef = useRef<HTMLTextAreaElement>(null); | 150 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 149 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); | 151 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 150 | const dragDepthRef = useRef(0); | 152 | const dragDepthRef = useRef(0); |
| ... | @@ -366,19 +368,30 @@ export default function ChatView() { | ... | @@ -366,19 +368,30 @@ export default function ChatView() { |
| 366 | // Auto-scroll on new messages | 368 | // Auto-scroll on new messages |
| 367 | const messageCount = activeThread?.messages.length ?? 0; | 369 | const messageCount = activeThread?.messages.length ?? 0; |
| 368 | const workLogCount = workLogEntries.length; | 370 | const workLogCount = workLogEntries.length; |
| 369 | useLayoutEffect(() => { | 371 | const scrollMessagesToBottom = useCallback((behavior: ScrollBehavior = "auto") => { |
| 370 | if (!activeThread?.id) return; | | |
| 371 | const scrollContainer = messagesScrollRef.current; | 372 | const scrollContainer = messagesScrollRef.current; |
| 372 | if (!scrollContainer) return; | 373 | if (!scrollContainer) return; |
| 373 | scrollContainer.scrollTop = scrollContainer.scrollHeight; | 374 | scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior }); |
| 374 | }, [activeThread?.id]); | 375 | shouldAutoScrollRef.current = true; |
| | 376 | }, []); |
| | 377 | const onMessagesScroll = useCallback(() => { |
| | 378 | const scrollContainer = messagesScrollRef.current; |
| | 379 | if (!scrollContainer) return; |
| | 380 | shouldAutoScrollRef.current = isScrollContainerNearBottom(scrollContainer); |
| | 381 | }, []); |
| | 382 | useLayoutEffect(() => { |
| | 383 | if (!activeThread?.id) return; |
| | 384 | scrollMessagesToBottom(); |
| | 385 | }, [activeThread?.id, scrollMessagesToBottom]); |
| 375 | useEffect(() => { | 386 | useEffect(() => { |
| 376 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | 387 | if (!shouldAutoScrollRef.current) return; |
| 377 | }, [messageCount]); | 388 | scrollMessagesToBottom("smooth"); |
| | 389 | }, [messageCount, scrollMessagesToBottom]); |
| 378 | useEffect(() => { | 390 | useEffect(() => { |
| 379 | if (phase !== "running") return; | 391 | if (phase !== "running") return; |
| 380 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | 392 | if (!shouldAutoScrollRef.current) return; |
| 381 | }, [phase, workLogCount]); | 393 | scrollMessagesToBottom("smooth"); |
| | 394 | }, [phase, workLogCount, scrollMessagesToBottom]); |
| 382 | | 395 | |
| 383 | useEffect(() => { | 396 | useEffect(() => { |
| 384 | setExpandedWorkGroups({}); | 397 | setExpandedWorkGroups({}); |
| ... | @@ -943,7 +956,11 @@ export default function ChatView() { | ... | @@ -943,7 +956,11 @@ export default function ChatView() { |
| 943 | )} | 956 | )} |
| 944 | | 957 | |
| 945 | {/* Messages */} | 958 | {/* Messages */} |
| 946 | <div ref={messagesScrollRef} className="min-h-0 flex-1 overflow-y-auto px-5 py-4"> | 959 | <div |
| | 960 | ref={messagesScrollRef} |
| | 961 | className="min-h-0 flex-1 overflow-y-auto px-5 py-4" |
| | 962 | onScroll={onMessagesScroll} |
| | 963 | > |
| 947 | {activeThread.messages.length === 0 && !isWorking ? ( | 964 | {activeThread.messages.length === 0 && !isWorking ? ( |
| 948 | <div className="flex h-full items-center justify-center"> | 965 | <div className="flex h-full items-center justify-center"> |
| 949 | <p className="text-sm text-muted-foreground/30"> | 966 | <p className="text-sm text-muted-foreground/30"> |