From 49df993bb4fa5c3590f2153a4425b6262dab42c6 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 12:35:21 +0200 Subject: [PATCH 01/36] conductor-checkpoint-start From 4ec8df40a85a4fc6697c51d198d0a204b8f2523e Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 12:39:28 +0200 Subject: [PATCH 02/36] conductor-checkpoint-msg_01LsFFaFLHSaS3kFZC6NhQUg --- src/features/session/hooks/useAutoScroll.ts | 83 ++++++++++++++++----- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 77731d480..f0e75a698 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, RefObject } from "react"; +import { useState, useEffect, useRef, RefObject } from "react"; import type { Message, SessionStatus } from "@/shared/types"; interface UseAutoScrollOptions { @@ -10,6 +10,12 @@ interface UseAutoScrollOptions { /** * Hook to manage auto-scroll behavior and scroll-to-bottom button + * + * Features: + * - Auto-scrolls when new messages arrive + * - Continuously scrolls during streaming (when content height changes) + * - Shows "scroll to bottom" button when user scrolls up + * - Respects user intent (doesn't auto-scroll if user manually scrolled up) */ export function useAutoScroll({ messages, @@ -18,42 +24,83 @@ export function useAutoScroll({ messagesEndRef, }: UseAutoScrollOptions) { const [showScrollButton, setShowScrollButton] = useState(false); - const [shouldAutoScroll, setShouldAutoScroll] = useState(true); + const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); + const lastScrollHeightRef = useRef(0); - // Auto-scroll when messages change or session status changes + // Check if user is near bottom (within threshold) + const isNearBottom = (threshold = 100) => { + const container = messagesContainerRef.current; + if (!container) return false; + + const { scrollTop, scrollHeight, clientHeight } = container; + return scrollHeight - scrollTop - clientHeight < threshold; + }; + + // Scroll to bottom function + const scrollToBottom = (smooth = false) => { + messagesEndRef.current?.scrollIntoView({ + behavior: smooth ? 'smooth' : 'auto', + block: 'end' + }); + }; + + // Auto-scroll when new messages arrive (only if user hasn't scrolled up) useEffect(() => { - if (shouldAutoScroll) { + if (!isUserScrolledUp) { scrollToBottom(); - setShouldAutoScroll(false); } - }, [messages, sessionStatus, shouldAutoScroll]); + }, [messages.length, isUserScrolledUp]); - // Handle scroll detection for "scroll to bottom" button + // Track user scroll behavior useEffect(() => { const container = messagesContainerRef.current; if (!container) return; const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = container; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; - setShowScrollButton(!isNearBottom); + const nearBottom = isNearBottom(100); + setShowScrollButton(!nearBottom); + setIsUserScrolledUp(!nearBottom); }; container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); }, [messagesContainerRef]); - function scrollToBottom(smooth = false) { - messagesEndRef.current?.scrollIntoView({ - behavior: smooth ? 'smooth' : 'auto', - block: 'end' + // ResizeObserver: Auto-scroll during streaming when content height changes + useEffect(() => { + const container = messagesContainerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const newHeight = entry.target.scrollHeight; + const oldHeight = lastScrollHeightRef.current; + + // Only auto-scroll if: + // 1. Content height increased (new content added) + // 2. User is near bottom (hasn't manually scrolled up) + // 3. Session is working (streaming in progress) + if ( + newHeight > oldHeight && + !isUserScrolledUp && + sessionStatus === 'working' + ) { + scrollToBottom(false); // Instant scroll for smooth streaming effect + } + + lastScrollHeightRef.current = newHeight; + } }); - } - function handleScrollToBottomClick() { - setShouldAutoScroll(true); + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, [messagesContainerRef, isUserScrolledUp, sessionStatus]); + + // Manual scroll to bottom (resets user scroll state) + const handleScrollToBottomClick = () => { + setIsUserScrolledUp(false); scrollToBottom(true); - } + }; return { showScrollButton, From ca6953728896b1d00e711f51bfccd24ffcf11d4e Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 12:40:27 +0200 Subject: [PATCH 03/36] conductor-checkpoint-msg_01RaSvwuq2b4DSfS6uW7Yfs8 --- src/features/session/hooks/useAutoScroll.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index f0e75a698..97433ddb7 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -44,10 +44,15 @@ export function useAutoScroll({ }); }; - // Auto-scroll when new messages arrive (only if user hasn't scrolled up) + // Auto-scroll when new messages arrive - scroll new message to TOP useEffect(() => { - if (!isUserScrolledUp) { - scrollToBottom(); + if (!isUserScrolledUp && messages.length > 0) { + // Scroll the last message to the TOP of the viewport + // This pushes previous messages up and out of view + messagesEndRef.current?.scrollIntoView({ + behavior: 'auto', + block: 'start', // Changed from 'end' to 'start' - positions at top + }); } }, [messages.length, isUserScrolledUp]); @@ -66,7 +71,7 @@ export function useAutoScroll({ return () => container.removeEventListener('scroll', handleScroll); }, [messagesContainerRef]); - // ResizeObserver: Auto-scroll during streaming when content height changes + // ResizeObserver: Continuous scroll during streaming useEffect(() => { const container = messagesContainerRef.current; if (!container) return; @@ -76,16 +81,14 @@ export function useAutoScroll({ const newHeight = entry.target.scrollHeight; const oldHeight = lastScrollHeightRef.current; - // Only auto-scroll if: - // 1. Content height increased (new content added) - // 2. User is near bottom (hasn't manually scrolled up) - // 3. Session is working (streaming in progress) + // During streaming, scroll down smoothly as content grows + // This keeps the streaming message visible and flowing if ( newHeight > oldHeight && !isUserScrolledUp && sessionStatus === 'working' ) { - scrollToBottom(false); // Instant scroll for smooth streaming effect + scrollToBottom(false); // Keep scrolling down during streaming } lastScrollHeightRef.current = newHeight; From 4925dd76c713f87a95b065ad0d233117b69f2943 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 12:57:45 +0200 Subject: [PATCH 04/36] conductor-checkpoint-msg_01AjxcMcQ3cat2JxS5Zyaz6u --- src/features/session/hooks/useAutoScroll.ts | 22 ++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 97433ddb7..508706520 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -44,15 +44,23 @@ export function useAutoScroll({ }); }; - // Auto-scroll when new messages arrive - scroll new message to TOP + // Auto-scroll when new messages arrive useEffect(() => { if (!isUserScrolledUp && messages.length > 0) { - // Scroll the last message to the TOP of the viewport - // This pushes previous messages up and out of view - messagesEndRef.current?.scrollIntoView({ - behavior: 'auto', - block: 'start', // Changed from 'end' to 'start' - positions at top - }); + const lastMessage = messages[messages.length - 1]; + + if (lastMessage.role === 'user') { + // USER message: Scroll to TOP of viewport + // This pushes previous messages up and out of view, focusing on the new user message + messagesEndRef.current?.scrollIntoView({ + behavior: 'auto', + block: 'start', + }); + } else { + // ASSISTANT message: Scroll to BOTTOM normally + // Keeps the full response visible as it streams + scrollToBottom(false); + } } }, [messages.length, isUserScrolledUp]); From 697319c178c0ca40e6e245af7c541bf53298c92e Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 12:59:55 +0200 Subject: [PATCH 05/36] conductor-checkpoint-msg_011KMPUd3Nk91fbLZw6pa9Hj --- src/features/session/hooks/useAutoScroll.ts | 33 +++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 508706520..6b6a62fac 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -12,10 +12,18 @@ interface UseAutoScrollOptions { * Hook to manage auto-scroll behavior and scroll-to-bottom button * * Features: - * - Auto-scrolls when new messages arrive - * - Continuously scrolls during streaming (when content height changes) + * - USER messages: Scroll to top of viewport (push old messages up) + * - ASSISTANT messages: Smart overflow detection + * - If there's visible space below → NO scroll (messages appear naturally) + * - If content would be hidden → AUTO scroll (reveal new content) * - Shows "scroll to bottom" button when user scrolls up * - Respects user intent (doesn't auto-scroll if user manually scrolled up) + * + * UX Benefits: + * - Reduces unnecessary scrolling when viewport has space + * - User sees their question + answer simultaneously + * - Less jarring, more natural content flow + * - Only scrolls when content would actually be cut off */ export function useAutoScroll({ messages, @@ -79,7 +87,8 @@ export function useAutoScroll({ return () => container.removeEventListener('scroll', handleScroll); }, [messagesContainerRef]); - // ResizeObserver: Continuous scroll during streaming + // ResizeObserver: Smart scroll during streaming + // Only scrolls if content would overflow viewport (be hidden) useEffect(() => { const container = messagesContainerRef.current; if (!container) return; @@ -89,14 +98,26 @@ export function useAutoScroll({ const newHeight = entry.target.scrollHeight; const oldHeight = lastScrollHeightRef.current; - // During streaming, scroll down smoothly as content grows - // This keeps the streaming message visible and flowing + // Only proceed if content grew and we're in a working session if ( newHeight > oldHeight && !isUserScrolledUp && sessionStatus === 'working' ) { - scrollToBottom(false); // Keep scrolling down during streaming + // Calculate visible boundaries + const { scrollTop, clientHeight } = container; + const viewportBottom = scrollTop + clientHeight; + + // Check if new content would be hidden below viewport + // Add small buffer (50px) to account for message input height + const contentBottom = newHeight; + const isContentHidden = contentBottom > viewportBottom + 50; + + // Only scroll if content would be cut off + if (isContentHidden) { + scrollToBottom(false); // Scroll to reveal hidden content + } + // Otherwise, let content appear naturally without scrolling } lastScrollHeightRef.current = newHeight; From eea5a2985f946337758f8dfdd932e0eab39e400b Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:01:03 +0200 Subject: [PATCH 06/36] conductor-checkpoint-msg_017wFpyFaQQwoePGwgBp1btm --- src/features/session/hooks/useAutoScroll.ts | 30 ++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 6b6a62fac..c54112871 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -6,6 +6,10 @@ interface UseAutoScrollOptions { sessionStatus: SessionStatus; messagesContainerRef: RefObject; messagesEndRef: RefObject; + // Configuration options + scrollThreshold?: number; // Distance from bottom to consider "at bottom" (default: 100) + inputHeightBuffer?: number; // Buffer for message input height (default: 80) + smoothScrollUser?: boolean; // Use smooth scroll for user messages (default: false) } /** @@ -30,13 +34,17 @@ export function useAutoScroll({ sessionStatus, messagesContainerRef, messagesEndRef, + scrollThreshold = 100, + inputHeightBuffer = 80, + smoothScrollUser = false, }: UseAutoScrollOptions) { const [showScrollButton, setShowScrollButton] = useState(false); const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); const lastScrollHeightRef = useRef(0); + const lastMessageCountRef = useRef(0); // Check if user is near bottom (within threshold) - const isNearBottom = (threshold = 100) => { + const isNearBottom = (threshold = scrollThreshold) => { const container = messagesContainerRef.current; if (!container) return false; @@ -54,14 +62,20 @@ export function useAutoScroll({ // Auto-scroll when new messages arrive useEffect(() => { - if (!isUserScrolledUp && messages.length > 0) { + // Only auto-scroll if a NEW message was added (not on initial mount) + if (messages.length === 0 || messages.length === lastMessageCountRef.current) { + lastMessageCountRef.current = messages.length; + return; + } + + if (!isUserScrolledUp) { const lastMessage = messages[messages.length - 1]; if (lastMessage.role === 'user') { // USER message: Scroll to TOP of viewport // This pushes previous messages up and out of view, focusing on the new user message messagesEndRef.current?.scrollIntoView({ - behavior: 'auto', + behavior: smoothScrollUser ? 'smooth' : 'auto', block: 'start', }); } else { @@ -70,7 +84,9 @@ export function useAutoScroll({ scrollToBottom(false); } } - }, [messages.length, isUserScrolledUp]); + + lastMessageCountRef.current = messages.length; + }, [messages.length, isUserScrolledUp, smoothScrollUser]); // Track user scroll behavior useEffect(() => { @@ -78,7 +94,7 @@ export function useAutoScroll({ if (!container) return; const handleScroll = () => { - const nearBottom = isNearBottom(100); + const nearBottom = isNearBottom(); setShowScrollButton(!nearBottom); setIsUserScrolledUp(!nearBottom); }; @@ -109,9 +125,9 @@ export function useAutoScroll({ const viewportBottom = scrollTop + clientHeight; // Check if new content would be hidden below viewport - // Add small buffer (50px) to account for message input height + // Add buffer to account for message input height const contentBottom = newHeight; - const isContentHidden = contentBottom > viewportBottom + 50; + const isContentHidden = contentBottom > viewportBottom + inputHeightBuffer; // Only scroll if content would be cut off if (isContentHidden) { From 22c8b4fb679bd54d989eade9a7b16bce9d9ec6fe Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:04:00 +0200 Subject: [PATCH 07/36] conductor-checkpoint-msg_0172L6ZcQHHGi73bba4RLNtu From 6fdf7423e683e6dcf48a50576ebeb4c31f15ede4 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:10:54 +0200 Subject: [PATCH 08/36] conductor-checkpoint-msg_01Kyq685uxYtQ8eYf2jc1oGx --- src/features/session/ui/Chat.tsx | 4 ++-- src/features/session/ui/MessageItem.tsx | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/features/session/ui/Chat.tsx b/src/features/session/ui/Chat.tsx index fbdd9769c..32eaf526e 100644 --- a/src/features/session/ui/Chat.tsx +++ b/src/features/session/ui/Chat.tsx @@ -55,12 +55,13 @@ export function Chat({ ) : ( <>
- {messages.map(message => ( + {messages.map((message, index) => ( ))}
@@ -74,7 +75,6 @@ export function Chat({ Claude is working... )} -
)} {showScrollButton && ( diff --git a/src/features/session/ui/MessageItem.tsx b/src/features/session/ui/MessageItem.tsx index 9e771210d..ce87c9815 100644 --- a/src/features/session/ui/MessageItem.tsx +++ b/src/features/session/ui/MessageItem.tsx @@ -5,6 +5,7 @@ * Automatically imports and registers all tool renderers. */ +import { forwardRef } from "react"; import type { Message } from "@/shared/types"; import type { ContentBlock } from "@/features/session/types"; import type { ToolResultMap } from "./chat-types"; @@ -23,7 +24,8 @@ interface MessageItemProps { toolResultMap: ToolResultMap; } -export function MessageItem({ message, parseContent, toolResultMap }: MessageItemProps) { +export const MessageItem = forwardRef( + ({ message, parseContent, toolResultMap }, ref) => { // Parse message content const contentBlocks = parseContent(message.content); @@ -77,6 +79,7 @@ export function MessageItem({ message, parseContent, toolResultMap }: MessageIte return (
); -} +}); + +MessageItem.displayName = 'MessageItem'; From d0c762bcc351e46dd6f3fa7e65c40c4382ca060d Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:11:39 +0200 Subject: [PATCH 09/36] conductor-checkpoint-msg_01GrXTUi9STrNCUrYrU51YLX From 4be9b23b7c07f34680b471652b9fb177468b5711 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:14:01 +0200 Subject: [PATCH 10/36] conductor-checkpoint-msg_018R21UGY63sLeAnA6iWdFGy From cf19fa80062d69630e7c6589692b19fcf4d3ad94 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:15:02 +0200 Subject: [PATCH 11/36] conductor-checkpoint-msg_0166UevwKoqLvWnB8Bta9g91 --- src/features/session/hooks/useAutoScroll.ts | 58 ++++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index c54112871..9465ad102 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -42,6 +42,7 @@ export function useAutoScroll({ const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); const lastScrollHeightRef = useRef(0); const lastMessageCountRef = useRef(0); + const isAutoScrollingRef = useRef(false); // Track if we're auto-scrolling // Check if user is near bottom (within threshold) const isNearBottom = (threshold = scrollThreshold) => { @@ -68,20 +69,40 @@ export function useAutoScroll({ return; } + const lastMessage = messages[messages.length - 1]; + + console.log('[useAutoScroll] New message:', { + role: lastMessage.role, + messageCount: messages.length, + isUserScrolledUp + }); + if (!isUserScrolledUp) { - const lastMessage = messages[messages.length - 1]; + isAutoScrollingRef.current = true; // Mark that we're auto-scrolling if (lastMessage.role === 'user') { // USER message: Scroll to TOP of viewport - // This pushes previous messages up and out of view, focusing on the new user message - messagesEndRef.current?.scrollIntoView({ - behavior: smoothScrollUser ? 'smooth' : 'auto', - block: 'start', - }); + console.log('[useAutoScroll] Scrolling user message to TOP'); + setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: smoothScrollUser ? 'smooth' : 'auto', + block: 'start', + }); + // Reset flag after scroll completes + setTimeout(() => { + isAutoScrollingRef.current = false; + }, 100); + }, 0); } else { // ASSISTANT message: Scroll to BOTTOM normally - // Keeps the full response visible as it streams - scrollToBottom(false); + console.log('[useAutoScroll] Scrolling assistant message to BOTTOM'); + setTimeout(() => { + scrollToBottom(false); + // Reset flag after scroll completes + setTimeout(() => { + isAutoScrollingRef.current = false; + }, 100); + }, 0); } } @@ -94,7 +115,14 @@ export function useAutoScroll({ if (!container) return; const handleScroll = () => { + // Don't update state if we're auto-scrolling + if (isAutoScrollingRef.current) { + console.log('[useAutoScroll] Ignoring scroll event (auto-scrolling)'); + return; + } + const nearBottom = isNearBottom(); + console.log('[useAutoScroll] User scroll detected, nearBottom:', nearBottom); setShowScrollButton(!nearBottom); setIsUserScrolledUp(!nearBottom); }; @@ -129,9 +157,23 @@ export function useAutoScroll({ const contentBottom = newHeight; const isContentHidden = contentBottom > viewportBottom + inputHeightBuffer; + console.log('[useAutoScroll] ResizeObserver:', { + newHeight, + oldHeight, + viewportBottom, + contentBottom, + isContentHidden, + sessionStatus + }); + // Only scroll if content would be cut off if (isContentHidden) { + console.log('[useAutoScroll] Content hidden, scrolling to bottom'); + isAutoScrollingRef.current = true; scrollToBottom(false); // Scroll to reveal hidden content + setTimeout(() => { + isAutoScrollingRef.current = false; + }, 100); } // Otherwise, let content appear naturally without scrolling } From 297e338d10a4364ba3401de8e979e9ed31366e93 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:19:25 +0200 Subject: [PATCH 12/36] conductor-checkpoint-msg_016VpEBRP4BpaFxuaehFcACu --- src/features/session/hooks/useAutoScroll.ts | 8 +++++--- src/features/session/ui/Chat.tsx | 5 ++++- src/features/session/ui/SessionPanel.tsx | 6 +++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 9465ad102..197899e83 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -5,7 +5,8 @@ interface UseAutoScrollOptions { messages: Message[]; sessionStatus: SessionStatus; messagesContainerRef: RefObject; - messagesEndRef: RefObject; + messagesEndRef: RefObject; // Empty div at end of messages + lastMessageRef: RefObject; // Last message element // Configuration options scrollThreshold?: number; // Distance from bottom to consider "at bottom" (default: 100) inputHeightBuffer?: number; // Buffer for message input height (default: 80) @@ -34,6 +35,7 @@ export function useAutoScroll({ sessionStatus, messagesContainerRef, messagesEndRef, + lastMessageRef, scrollThreshold = 100, inputHeightBuffer = 80, smoothScrollUser = false, @@ -81,10 +83,10 @@ export function useAutoScroll({ isAutoScrollingRef.current = true; // Mark that we're auto-scrolling if (lastMessage.role === 'user') { - // USER message: Scroll to TOP of viewport + // USER message: Scroll to TOP of viewport using lastMessageRef console.log('[useAutoScroll] Scrolling user message to TOP'); setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ + lastMessageRef.current?.scrollIntoView({ behavior: smoothScrollUser ? 'smooth' : 'auto', block: 'start', }); diff --git a/src/features/session/ui/Chat.tsx b/src/features/session/ui/Chat.tsx index 32eaf526e..e2bfcc2c3 100644 --- a/src/features/session/ui/Chat.tsx +++ b/src/features/session/ui/Chat.tsx @@ -13,6 +13,7 @@ interface ChatProps { sessionStatus: SessionStatus; parseContent: (content: string) => ReactNode; messagesEndRef: RefObject; + lastMessageRef: RefObject; messagesContainerRef: RefObject; showScrollButton?: boolean; onScrollToBottom?: () => void; @@ -25,6 +26,7 @@ export function Chat({ sessionStatus, parseContent, messagesEndRef, + lastMessageRef, messagesContainerRef, showScrollButton = false, onScrollToBottom, @@ -61,7 +63,7 @@ export function Chat({ message={message} parseContent={parseContent} toolResultMap={toolResultMap} - ref={index === messages.length - 1 ? messagesEndRef : undefined} + ref={index === messages.length - 1 ? lastMessageRef : undefined} /> ))}
@@ -75,6 +77,7 @@ export function Chat({ Claude is working... )} +
)} {showScrollButton && ( diff --git a/src/features/session/ui/SessionPanel.tsx b/src/features/session/ui/SessionPanel.tsx index 9b8ae461a..dcd9685b9 100644 --- a/src/features/session/ui/SessionPanel.tsx +++ b/src/features/session/ui/SessionPanel.tsx @@ -38,7 +38,8 @@ export interface SessionPanelRef { export const SessionPanel = forwardRef( ({ sessionId, onClose, embedded = false, onCompact, onCreatePR, onStop }, ref) => { const [selectedFile, setSelectedFile] = useState(null); - const messagesEndRef = useRef(null); + const messagesEndRef = useRef(null); // Empty div at end for scrolling to bottom + const lastMessageRef = useRef(null); // Last message element for scrolling to top const messagesContainerRef = useRef(null); // Custom hooks (useSocket manages socket connection lifecycle) @@ -171,6 +172,7 @@ export const SessionPanel = forwardRef( sessionStatus, messagesContainerRef, messagesEndRef, + lastMessageRef, }); // Expose action handlers to parent @@ -255,6 +257,7 @@ export const SessionPanel = forwardRef( sessionStatus={sessionStatus} parseContent={parseContent} messagesEndRef={messagesEndRef} + lastMessageRef={lastMessageRef} messagesContainerRef={messagesContainerRef} showScrollButton={showScrollButton} onScrollToBottom={handleScrollToBottomClick} @@ -327,6 +330,7 @@ export const SessionPanel = forwardRef( sessionStatus={sessionStatus} parseContent={parseContent} messagesEndRef={messagesEndRef} + lastMessageRef={lastMessageRef} messagesContainerRef={messagesContainerRef} showScrollButton={showScrollButton} onScrollToBottom={handleScrollToBottomClick} From 94d646a5ce875067dbfccc15a40ae390d61e18e1 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:22:40 +0200 Subject: [PATCH 13/36] conductor-checkpoint-msg_01KwrFsVPiDqwh5uJ9k1qDRg --- src/features/session/hooks/useAutoScroll.ts | 47 +++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 197899e83..65f838ef5 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -76,7 +76,9 @@ export function useAutoScroll({ console.log('[useAutoScroll] New message:', { role: lastMessage.role, messageCount: messages.length, - isUserScrolledUp + isUserScrolledUp, + lastMessageRef: lastMessageRef.current, + messagesEndRef: messagesEndRef.current }); if (!isUserScrolledUp) { @@ -84,28 +86,49 @@ export function useAutoScroll({ if (lastMessage.role === 'user') { // USER message: Scroll to TOP of viewport using lastMessageRef - console.log('[useAutoScroll] Scrolling user message to TOP'); - setTimeout(() => { - lastMessageRef.current?.scrollIntoView({ - behavior: smoothScrollUser ? 'smooth' : 'auto', - block: 'start', - }); + console.log('[useAutoScroll] Scrolling user message to TOP', { + refExists: !!lastMessageRef.current, + refElement: lastMessageRef.current + }); + + // Use requestAnimationFrame for better timing + requestAnimationFrame(() => { + if (lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({ + behavior: smoothScrollUser ? 'smooth' : 'auto', + block: 'start', + }); + console.log('[useAutoScroll] User message scrolled'); + } else { + console.error('[useAutoScroll] lastMessageRef is null!'); + } // Reset flag after scroll completes setTimeout(() => { isAutoScrollingRef.current = false; }, 100); - }, 0); + }); } else { // ASSISTANT message: Scroll to BOTTOM normally - console.log('[useAutoScroll] Scrolling assistant message to BOTTOM'); - setTimeout(() => { - scrollToBottom(false); + console.log('[useAutoScroll] Scrolling assistant message to BOTTOM', { + refExists: !!messagesEndRef.current, + refElement: messagesEndRef.current + }); + + requestAnimationFrame(() => { + if (messagesEndRef.current) { + scrollToBottom(false); + console.log('[useAutoScroll] Assistant message scrolled'); + } else { + console.error('[useAutoScroll] messagesEndRef is null!'); + } // Reset flag after scroll completes setTimeout(() => { isAutoScrollingRef.current = false; }, 100); - }, 0); + }); } + } else { + console.log('[useAutoScroll] Skipping auto-scroll - user scrolled up'); } lastMessageCountRef.current = messages.length; From bee10d6b64d698ffd89774145211ee71895b8775 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:30:25 +0200 Subject: [PATCH 14/36] conductor-checkpoint-msg_01DRfqcHstQtxgXHLmLexgCv --- src/features/session/ui/Chat.tsx | 18 +++++++++++------- src/features/session/ui/MessageItem.tsx | 9 ++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/features/session/ui/Chat.tsx b/src/features/session/ui/Chat.tsx index e2bfcc2c3..6d787cfc6 100644 --- a/src/features/session/ui/Chat.tsx +++ b/src/features/session/ui/Chat.tsx @@ -58,13 +58,17 @@ export function Chat({ <>
{messages.map((message, index) => ( - +
+ + {/* Invisible ref marker - always renders even if MessageItem returns null */} + {index === messages.length - 1 && ( +
+ )} +
))}
{sessionStatus === 'working' && ( diff --git a/src/features/session/ui/MessageItem.tsx b/src/features/session/ui/MessageItem.tsx index ce87c9815..9e771210d 100644 --- a/src/features/session/ui/MessageItem.tsx +++ b/src/features/session/ui/MessageItem.tsx @@ -5,7 +5,6 @@ * Automatically imports and registers all tool renderers. */ -import { forwardRef } from "react"; import type { Message } from "@/shared/types"; import type { ContentBlock } from "@/features/session/types"; import type { ToolResultMap } from "./chat-types"; @@ -24,8 +23,7 @@ interface MessageItemProps { toolResultMap: ToolResultMap; } -export const MessageItem = forwardRef( - ({ message, parseContent, toolResultMap }, ref) => { +export function MessageItem({ message, parseContent, toolResultMap }: MessageItemProps) { // Parse message content const contentBlocks = parseContent(message.content); @@ -79,7 +77,6 @@ export const MessageItem = forwardRef( return (
(
); -}); - -MessageItem.displayName = 'MessageItem'; +} From ab40ee72665badb0a7bbbfc74d7de15fecf20eb0 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:50:28 +0200 Subject: [PATCH 15/36] conductor-checkpoint-msg_01KwEyACFmCpdb34U7iPZcsX --- src/features/session/hooks/useAutoScroll.ts | 127 ++++++-------------- src/features/session/ui/MessageInput.tsx | 17 ++- 2 files changed, 49 insertions(+), 95 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index 65f838ef5..cde7d711a 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -42,7 +42,6 @@ export function useAutoScroll({ }: UseAutoScrollOptions) { const [showScrollButton, setShowScrollButton] = useState(false); const [isUserScrolledUp, setIsUserScrolledUp] = useState(false); - const lastScrollHeightRef = useRef(0); const lastMessageCountRef = useRef(0); const isAutoScrollingRef = useRef(false); // Track if we're auto-scrolling @@ -72,63 +71,42 @@ export function useAutoScroll({ } const lastMessage = messages[messages.length - 1]; + const container = messagesContainerRef.current; - console.log('[useAutoScroll] New message:', { - role: lastMessage.role, - messageCount: messages.length, - isUserScrolledUp, - lastMessageRef: lastMessageRef.current, - messagesEndRef: messagesEndRef.current - }); - - if (!isUserScrolledUp) { - isAutoScrollingRef.current = true; // Mark that we're auto-scrolling + if (!isUserScrolledUp && container) { + isAutoScrollingRef.current = true; if (lastMessage.role === 'user') { - // USER message: Scroll to TOP of viewport using lastMessageRef - console.log('[useAutoScroll] Scrolling user message to TOP', { - refExists: !!lastMessageRef.current, - refElement: lastMessageRef.current - }); - - // Use requestAnimationFrame for better timing + // USER message: Scroll marker to TOP of viewport requestAnimationFrame(() => { - if (lastMessageRef.current) { - lastMessageRef.current.scrollIntoView({ - behavior: smoothScrollUser ? 'smooth' : 'auto', - block: 'start', - }); - console.log('[useAutoScroll] User message scrolled'); + const marker = lastMessageRef.current; + if (marker) { + // Get marker position relative to container + const markerTop = marker.offsetTop; + // Scroll container so marker is at the top + container.scrollTop = markerTop; + + if (import.meta.env.DEV) { + console.log('[useAutoScroll] User message scrolled to top:', markerTop); + } } else { console.error('[useAutoScroll] lastMessageRef is null!'); } - // Reset flag after scroll completes + setTimeout(() => { isAutoScrollingRef.current = false; }, 100); }); } else { - // ASSISTANT message: Scroll to BOTTOM normally - console.log('[useAutoScroll] Scrolling assistant message to BOTTOM', { - refExists: !!messagesEndRef.current, - refElement: messagesEndRef.current - }); - + // ASSISTANT message: Scroll to BOTTOM requestAnimationFrame(() => { - if (messagesEndRef.current) { - scrollToBottom(false); - console.log('[useAutoScroll] Assistant message scrolled'); - } else { - console.error('[useAutoScroll] messagesEndRef is null!'); - } - // Reset flag after scroll completes + scrollToBottom(false); + setTimeout(() => { isAutoScrollingRef.current = false; }, 100); }); } - } else { - console.log('[useAutoScroll] Skipping auto-scroll - user scrolled up'); } lastMessageCountRef.current = messages.length; @@ -141,13 +119,9 @@ export function useAutoScroll({ const handleScroll = () => { // Don't update state if we're auto-scrolling - if (isAutoScrollingRef.current) { - console.log('[useAutoScroll] Ignoring scroll event (auto-scrolling)'); - return; - } + if (isAutoScrollingRef.current) return; const nearBottom = isNearBottom(); - console.log('[useAutoScroll] User scroll detected, nearBottom:', nearBottom); setShowScrollButton(!nearBottom); setIsUserScrolledUp(!nearBottom); }; @@ -156,60 +130,33 @@ export function useAutoScroll({ return () => container.removeEventListener('scroll', handleScroll); }, [messagesContainerRef]); - // ResizeObserver: Smart scroll during streaming - // Only scrolls if content would overflow viewport (be hidden) + // ResizeObserver: Auto-scroll during streaming (when content grows) useEffect(() => { const container = messagesContainerRef.current; if (!container) return; - const resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const newHeight = entry.target.scrollHeight; - const oldHeight = lastScrollHeightRef.current; - - // Only proceed if content grew and we're in a working session - if ( - newHeight > oldHeight && - !isUserScrolledUp && - sessionStatus === 'working' - ) { - // Calculate visible boundaries - const { scrollTop, clientHeight } = container; - const viewportBottom = scrollTop + clientHeight; - - // Check if new content would be hidden below viewport - // Add buffer to account for message input height - const contentBottom = newHeight; - const isContentHidden = contentBottom > viewportBottom + inputHeightBuffer; - - console.log('[useAutoScroll] ResizeObserver:', { - newHeight, - oldHeight, - viewportBottom, - contentBottom, - isContentHidden, - sessionStatus - }); - - // Only scroll if content would be cut off - if (isContentHidden) { - console.log('[useAutoScroll] Content hidden, scrolling to bottom'); - isAutoScrollingRef.current = true; - scrollToBottom(false); // Scroll to reveal hidden content - setTimeout(() => { - isAutoScrollingRef.current = false; - }, 100); - } - // Otherwise, let content appear naturally without scrolling - } - - lastScrollHeightRef.current = newHeight; + const resizeObserver = new ResizeObserver(() => { + const { scrollTop, scrollHeight, clientHeight } = container; + const viewportBottom = scrollTop + clientHeight; + const contentBottom = scrollHeight; + const isContentHidden = contentBottom > viewportBottom + inputHeightBuffer; + + // Only auto-scroll if: + // 1. In working session (streaming) + // 2. User hasn't scrolled up + // 3. Content would be hidden below viewport + if (sessionStatus === 'working' && !isUserScrolledUp && isContentHidden) { + isAutoScrollingRef.current = true; + scrollToBottom(false); + setTimeout(() => { + isAutoScrollingRef.current = false; + }, 50); } }); resizeObserver.observe(container); return () => resizeObserver.disconnect(); - }, [messagesContainerRef, isUserScrolledUp, sessionStatus]); + }, [messagesContainerRef, isUserScrolledUp, sessionStatus, inputHeightBuffer]); // Manual scroll to bottom (resets user scroll state) const handleScrollToBottomClick = () => { diff --git a/src/features/session/ui/MessageInput.tsx b/src/features/session/ui/MessageInput.tsx index 09b6dcf58..b6983ae0b 100644 --- a/src/features/session/ui/MessageInput.tsx +++ b/src/features/session/ui/MessageInput.tsx @@ -43,8 +43,11 @@ export function MessageInput({ const resizeTextarea = () => { const el = textareaRef.current; if (!el) return; - el.style.height = 'auto'; - el.style.height = Math.min(el.scrollHeight, 200) + 'px'; + // Reset to minimum first + el.style.height = '40px'; + // Then expand to fit content, max 200px + const newHeight = Math.min(Math.max(el.scrollHeight, 40), 200); + el.style.height = newHeight + 'px'; }; // Auto-resize textarea @@ -52,8 +55,12 @@ export function MessageInput({ resizeTextarea(); }, [messageInput]); + // Set initial height on mount useEffect(() => { - resizeTextarea(); + const el = textareaRef.current; + if (el) { + el.style.height = '40px'; + } }, []); return ( @@ -86,15 +93,15 @@ export function MessageInput({ resizeTextarea(); }} onBlur={() => setIsFocused(false)} + style={{ height: '40px' }} className=" flex-1 bg-transparent border-none outline-none resize-none text-body-lg text-foreground placeholder:text-muted-foreground - min-h-[24px] max-h-[200px] + max-h-[200px] font-sans overflow-y-auto scrollbar-vibrancy " - rows={1} /> {/* Action Buttons */} From ce4d094e2e22640de143b03a4e888417b6bd0f8e Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 13:58:41 +0200 Subject: [PATCH 16/36] conductor-checkpoint-msg_019Qcq9d9uFwPAcMxUUvDWK9 From 6de859c9a755804b13651974cfcdbf9b404d9caa Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 14:01:54 +0200 Subject: [PATCH 17/36] conductor-checkpoint-msg_01DvBTmKJP7tkjusVAGte15G From b8cf5ded9ffba8cc25b594415ef4c7841fdb82ee Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 14:03:54 +0200 Subject: [PATCH 18/36] conductor-checkpoint-msg_01VDXabb7JcrLDmeuHdfiJ3T From fedaacac4c8f611c0b5cddb7d381eb08624bb6cb Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 14:06:55 +0200 Subject: [PATCH 19/36] fix: Address code review feedback for auto-scroll implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - Move ref to wrapper div (fixes user message scroll bug) - Change ResizeObserver to observe content instead of container Accessibility improvements: - Add reduced motion support for scroll animations - Add motion-reduce classes for pulse/spin animations - Check prefers-reduced-motion in scroll behaviors Code cleanup: - Remove redundant mount effect in MessageInput - Improve console logging (error → warn for nulls) - Simplify textarea height management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/features/session/hooks/useAutoScroll.ts | 20 ++++++++++++++------ src/features/session/ui/Chat.tsx | 15 +++++++-------- src/features/session/ui/MessageInput.tsx | 8 -------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index cde7d711a..a6fb72611 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -84,13 +84,19 @@ export function useAutoScroll({ // Get marker position relative to container const markerTop = marker.offsetTop; // Scroll container so marker is at the top - container.scrollTop = markerTop; + const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + container.scrollTo({ + top: Math.max(0, markerTop), + behavior: smoothScrollUser && !prefersReduced ? 'smooth' : 'auto', + }); if (import.meta.env.DEV) { console.log('[useAutoScroll] User message scrolled to top:', markerTop); } } else { - console.error('[useAutoScroll] lastMessageRef is null!'); + if (import.meta.env.DEV) { + console.warn('[useAutoScroll] lastMessageRef is null!'); + } } setTimeout(() => { @@ -133,7 +139,8 @@ export function useAutoScroll({ // ResizeObserver: Auto-scroll during streaming (when content grows) useEffect(() => { const container = messagesContainerRef.current; - if (!container) return; + const target = lastMessageRef.current ?? messagesEndRef.current; + if (!container || !target) return; const resizeObserver = new ResizeObserver(() => { const { scrollTop, scrollHeight, clientHeight } = container; @@ -154,14 +161,15 @@ export function useAutoScroll({ } }); - resizeObserver.observe(container); + resizeObserver.observe(target); return () => resizeObserver.disconnect(); - }, [messagesContainerRef, isUserScrolledUp, sessionStatus, inputHeightBuffer]); + }, [messagesContainerRef, messagesEndRef, lastMessageRef, isUserScrolledUp, sessionStatus, inputHeightBuffer]); // Manual scroll to bottom (resets user scroll state) const handleScrollToBottomClick = () => { setIsUserScrolledUp(false); - scrollToBottom(true); + const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + scrollToBottom(!prefersReduced); }; return { diff --git a/src/features/session/ui/Chat.tsx b/src/features/session/ui/Chat.tsx index 6d787cfc6..e1b69133b 100644 --- a/src/features/session/ui/Chat.tsx +++ b/src/features/session/ui/Chat.tsx @@ -37,7 +37,7 @@ export function Chat({ id="chat-messages" role="log" aria-live="polite" - className="relative flex-1 overflow-y-auto overflow-x-hidden scroll-smooth min-h-0 px-6 pt-6" + className="relative flex-1 overflow-y-auto overflow-x-hidden scroll-smooth motion-reduce:scroll-auto min-h-0 px-6 pt-6" ref={messagesContainerRef} > {loading ? ( @@ -58,16 +58,15 @@ export function Chat({ <>
{messages.map((message, index) => ( -
+
- {/* Invisible ref marker - always renders even if MessageItem returns null */} - {index === messages.length - 1 && ( -
- )}
))}
@@ -75,9 +74,9 @@ export function Chat({
- + Claude is working...
)} diff --git a/src/features/session/ui/MessageInput.tsx b/src/features/session/ui/MessageInput.tsx index b6983ae0b..efa87380d 100644 --- a/src/features/session/ui/MessageInput.tsx +++ b/src/features/session/ui/MessageInput.tsx @@ -55,14 +55,6 @@ export function MessageInput({ resizeTextarea(); }, [messageInput]); - // Set initial height on mount - useEffect(() => { - const el = textareaRef.current; - if (el) { - el.style.height = '40px'; - } - }, []); - return (
{/* Glassmorphic ChatBox */} From 15d5b145fd1f5bfad8b4ccf636ed0b5cb2fbee15 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 15:19:56 +0200 Subject: [PATCH 20/36] conductor-checkpoint-msg_016uuo7LPRTUr9qux5xBXwd7 From 310830204c0fa58b3b609e7618d1aea39d8e3bad Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 15:21:39 +0200 Subject: [PATCH 21/36] conductor-checkpoint-msg_01RGMGrchWcmqM1gt4Pa6rTa From 5ab0521caf89742751a420aa9798c8151737e606 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 15:23:20 +0200 Subject: [PATCH 22/36] fix: Resolve scroll button flicker and inconsistent timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: - Scroll button flicker: Changed from sticky+padding to absolute positioning - Removes layout shift when button shows/hides - Button no longer affects document flow - Consistent timeouts: Extract AUTO_SCROLL_RESET_DELAY constant (100ms) - All auto-scroll flag resets now use same delay - Previously mixed 50ms and 100ms delays Changes: - Chat.tsx: Button uses absolute positioning (no pb-6) - useAutoScroll.ts: Single constant for all setTimeout delays 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/features/session/hooks/useAutoScroll.ts | 9 ++++++--- src/features/session/ui/Chat.tsx | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/features/session/hooks/useAutoScroll.ts b/src/features/session/hooks/useAutoScroll.ts index a6fb72611..6fc11fd99 100644 --- a/src/features/session/hooks/useAutoScroll.ts +++ b/src/features/session/hooks/useAutoScroll.ts @@ -13,6 +13,9 @@ interface UseAutoScrollOptions { smoothScrollUser?: boolean; // Use smooth scroll for user messages (default: false) } +// Constants +const AUTO_SCROLL_RESET_DELAY = 100; // ms - delay before resetting auto-scroll flag + /** * Hook to manage auto-scroll behavior and scroll-to-bottom button * @@ -101,7 +104,7 @@ export function useAutoScroll({ setTimeout(() => { isAutoScrollingRef.current = false; - }, 100); + }, AUTO_SCROLL_RESET_DELAY); }); } else { // ASSISTANT message: Scroll to BOTTOM @@ -110,7 +113,7 @@ export function useAutoScroll({ setTimeout(() => { isAutoScrollingRef.current = false; - }, 100); + }, AUTO_SCROLL_RESET_DELAY); }); } } @@ -157,7 +160,7 @@ export function useAutoScroll({ scrollToBottom(false); setTimeout(() => { isAutoScrollingRef.current = false; - }, 50); + }, AUTO_SCROLL_RESET_DELAY); } }); diff --git a/src/features/session/ui/Chat.tsx b/src/features/session/ui/Chat.tsx index e1b69133b..562776c89 100644 --- a/src/features/session/ui/Chat.tsx +++ b/src/features/session/ui/Chat.tsx @@ -84,11 +84,11 @@ export function Chat({ )} {showScrollButton && ( -
+
-
- )}
); } diff --git a/src/features/session/ui/SessionPanel.tsx b/src/features/session/ui/SessionPanel.tsx index dcd9685b9..fcfaec334 100644 --- a/src/features/session/ui/SessionPanel.tsx +++ b/src/features/session/ui/SessionPanel.tsx @@ -20,7 +20,7 @@ import { useStopSession, } from "../api/session.queries"; import { Button } from "@/components/ui/button"; -import { X, ArrowLeft } from "lucide-react"; +import { X, ArrowLeft, ChevronDown } from "lucide-react"; interface SessionPanelProps { sessionId: string; @@ -259,11 +259,26 @@ export const SessionPanel = forwardRef( messagesEndRef={messagesEndRef} lastMessageRef={lastMessageRef} messagesContainerRef={messagesContainerRef} - showScrollButton={showScrollButton} - onScrollToBottom={handleScrollToBottomClick} toolResultMap={toolResultMap} /> + {/* Scroll to bottom button */} + {showScrollButton && ( +
+ +
+ )} + {/* Message Input - Sticky at bottom */} (
) : ( // Show message timeline - Chat + Input - <> +
( messagesEndRef={messagesEndRef} lastMessageRef={lastMessageRef} messagesContainerRef={messagesContainerRef} - showScrollButton={showScrollButton} - onScrollToBottom={handleScrollToBottomClick} toolResultMap={toolResultMap} /> + {/* Scroll to bottom button */} + {showScrollButton && ( +
+ +
+ )} + {/* Message Input - Sticky at bottom */} ( onCreatePR={createPR} onStop={stopSession} /> - +
)}
From da147c87aae38092f869ebef3cc91c2be29ffed0 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 15:33:45 +0200 Subject: [PATCH 25/36] conductor-checkpoint-msg_013LrByDTmdgZGGpzgNGUqWt From 7b6761988791daf6a6a2d0f6adae52a5dbee4765 Mon Sep 17 00:00:00 2001 From: Adam Zvada Date: Wed, 22 Oct 2025 15:34:41 +0200 Subject: [PATCH 26/36] fix: Adjust scroll button position to prevent cropping Changed bottom-20 to bottom-28 for more clearance above message input. This prevents the button from being cropped by the input field. --- src/features/session/ui/SessionPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/session/ui/SessionPanel.tsx b/src/features/session/ui/SessionPanel.tsx index fcfaec334..b9cf820f0 100644 --- a/src/features/session/ui/SessionPanel.tsx +++ b/src/features/session/ui/SessionPanel.tsx @@ -264,7 +264,7 @@ export const SessionPanel = forwardRef( {/* Scroll to bottom button */} {showScrollButton && ( -
+