Story 22: Implement smart auto-scroll that respects user scrolling

User Story:
As a user, I want to be able to scroll up to review previous messages
while the AI is streaming or adding new content, without being
constantly dragged back to the bottom.

Implementation:
- Replaced position-based threshold detection with user-intent tracking
- Detects when user scrolls UP and disables auto-scroll completely
- Auto-scroll only re-enables when user manually returns to bottom (<5px)
- Uses refs to track scroll position and direction for smooth operation
- Works seamlessly during rapid token streaming and tool execution

Technical Details:
- lastScrollTopRef: Tracks previous scroll position to detect direction
- userScrolledUpRef: Flag set when upward scrolling is detected
- Direct scrollTop manipulation for instant, non-fighting scroll behavior
- Threshold of 5px from absolute bottom to re-enable auto-scroll

Spec Updates:
- Added comprehensive Smart Auto-Scroll section to UI_UX.md
- Documented the problem, solution, requirements, and implementation
- Includes code examples and edge case handling

Acceptance Criteria Met:
 Auto-scroll disabled when scrolling up
 Auto-scroll resumes when returning to bottom
 Works normally when already at bottom
 Smooth detection without flickering
 Works during streaming and tool execution

Files Changed:
- src/components/Chat.tsx: Implemented user-intent tracking
- .living_spec/specs/functional/UI_UX.md: Added Smart Auto-Scroll spec
- .living_spec/stories/22_smart_autoscroll.md: Marked complete
This commit is contained in:
Dave
2025-12-27 19:21:34 +00:00
parent 1baf3fa728
commit 57826dc5ee
4 changed files with 150 additions and 8 deletions

View File

@@ -22,6 +22,10 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [streamingContent, setStreamingContent] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const shouldAutoScrollRef = useRef(true);
const lastScrollTopRef = useRef(0);
const userScrolledUpRef = useRef(false);
// Token estimation and context window tracking
const estimateTokens = (text: string): number => {
@@ -118,11 +122,43 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
}, []);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
const element = scrollContainerRef.current;
if (element) {
element.scrollTop = element.scrollHeight;
lastScrollTopRef.current = element.scrollHeight;
}
};
const handleScroll = () => {
const element = scrollContainerRef.current;
if (!element) return;
const currentScrollTop = element.scrollTop;
const isAtBottom =
element.scrollHeight - element.scrollTop - element.clientHeight < 5;
// Detect if user scrolled UP
if (currentScrollTop < lastScrollTopRef.current) {
userScrolledUpRef.current = true;
shouldAutoScrollRef.current = false;
}
// If user scrolled back to bottom, re-enable auto-scroll
if (isAtBottom) {
userScrolledUpRef.current = false;
shouldAutoScrollRef.current = true;
}
lastScrollTopRef.current = currentScrollTop;
};
// Smart auto-scroll: only scroll if user hasn't scrolled up
// biome-ignore lint/correctness/useExhaustiveDependencies: We intentionally trigger on messages/streamingContent changes
useEffect(scrollToBottom, [messages, streamingContent]);
useEffect(() => {
if (shouldAutoScrollRef.current && !userScrolledUpRef.current) {
scrollToBottom();
}
}, [messages, streamingContent]);
useEffect(() => {
inputRef.current?.focus();
@@ -419,6 +455,8 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
{/* Messages Area */}
<div
ref={scrollContainerRef}
onScroll={handleScroll}
style={{
flex: 1,
overflowY: "auto",