diff --git a/packages/acp-client/src/components/AgentPanel.test.tsx b/packages/acp-client/src/components/AgentPanel.test.tsx index a361d91..66c4702 100644 --- a/packages/acp-client/src/components/AgentPanel.test.tsx +++ b/packages/acp-client/src/components/AgentPanel.test.tsx @@ -276,7 +276,7 @@ describe('AgentPanel auto-scroll behavior', () => { // is the right element. expect(scrollContainer).toBeTruthy(); expect(scrollContainer.classList.contains('overflow-y-auto')).toBe(true); - expect(scrollContainer.classList.contains('flex-1')).toBe(true); + expect(scrollContainer.classList.contains('h-full')).toBe(true); }); it('renders streaming messages correctly in scroll container', () => { @@ -507,3 +507,77 @@ describe('AgentPanel scroll reset on replay', () => { expect(resetToBottomSpy).not.toHaveBeenCalled(); }); }); + +// ============================================================================= +// Scroll-to-bottom FAB tests +// ============================================================================= + +describe('AgentPanel scroll-to-bottom FAB', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let autoScrollSpy: any; + let scrollToBottomSpy: ReturnType; + + function setupMock(isAtBottom: boolean) { + scrollToBottomSpy = vi.fn(); + autoScrollSpy = vi.spyOn(autoScrollModule, 'useAutoScroll').mockReturnValue({ + scrollRef: vi.fn(), + isAtBottom, + scrollToBottom: scrollToBottomSpy, + resetToBottom: vi.fn(), + }); + } + + afterEach(() => { + autoScrollSpy?.mockRestore(); + }); + + it('shows scroll-to-bottom button when not at bottom and messages exist', () => { + setupMock(false); + const session = createMockSession(); + const items: ConversationItem[] = [ + { kind: 'user_message', id: '1', text: 'Hello', timestamp: Date.now() }, + ]; + const messages = createMockMessages({ items }); + + render(); + + expect(screen.getByLabelText('Scroll to bottom')).toBeTruthy(); + }); + + it('hides scroll-to-bottom button when at bottom', () => { + setupMock(true); + const session = createMockSession(); + const items: ConversationItem[] = [ + { kind: 'user_message', id: '1', text: 'Hello', timestamp: Date.now() }, + ]; + const messages = createMockMessages({ items }); + + render(); + + expect(screen.queryByLabelText('Scroll to bottom')).toBeNull(); + }); + + it('hides scroll-to-bottom button when no messages', () => { + setupMock(false); + const session = createMockSession(); + const messages = createMockMessages({ items: [] }); + + render(); + + expect(screen.queryByLabelText('Scroll to bottom')).toBeNull(); + }); + + it('calls scrollToBottom when FAB is clicked', () => { + setupMock(false); + const session = createMockSession(); + const items: ConversationItem[] = [ + { kind: 'user_message', id: '1', text: 'Hello', timestamp: Date.now() }, + ]; + const messages = createMockMessages({ items }); + + render(); + + fireEvent.click(screen.getByLabelText('Scroll to bottom')); + expect(scrollToBottomSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/acp-client/src/components/AgentPanel.tsx b/packages/acp-client/src/components/AgentPanel.tsx index be9af7d..6a0a92c 100644 --- a/packages/acp-client/src/components/AgentPanel.tsx +++ b/packages/acp-client/src/components/AgentPanel.tsx @@ -80,7 +80,7 @@ export const AgentPanel = React.forwardRef(fu const [showPalette, setShowPalette] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showPlanModal, setShowPlanModal] = useState(false); - const { scrollRef, resetToBottom } = useAutoScroll(); + const { scrollRef, isAtBottom, scrollToBottom, resetToBottom } = useAutoScroll(); const inputRef = useRef(null); const paletteRef = useRef(null); @@ -281,15 +281,31 @@ export const AgentPanel = React.forwardRef(fu )} {/* Message area */} -
- {messages.items.length === 0 && !isReconnecting && ( -
- Send a message to start the conversation -
+
+
+ {messages.items.length === 0 && !isReconnecting && ( +
+ Send a message to start the conversation +
+ )} + {messages.items.map((item) => ( + + ))} +
+ {/* Scroll-to-bottom FAB */} + {!isAtBottom && messages.items.length > 0 && ( + )} - {messages.items.map((item) => ( - - ))}
{/* Input area */}