diff --git a/webui/src/components/thread/ThreadViewport.tsx b/webui/src/components/thread/ThreadViewport.tsx index ae10c8d5..21330798 100644 --- a/webui/src/components/thread/ThreadViewport.tsx +++ b/webui/src/components/thread/ThreadViewport.tsx @@ -532,7 +532,7 @@ export const ThreadViewport = forwardRef
-
-
+
+
{emptyState}
{composer}
diff --git a/webui/src/tests/thread-viewport.test.tsx b/webui/src/tests/thread-viewport.test.tsx index acd087d2..6ae3e815 100644 --- a/webui/src/tests/thread-viewport.test.tsx +++ b/webui/src/tests/thread-viewport.test.tsx @@ -598,6 +598,77 @@ describe("ThreadViewport", () => { } }); + it("keeps the welcome composer above a mobile soft keyboard", async () => { + const visualViewport = stubVisualViewport({ innerHeight: 800, height: 480 }); + try { + const { container } = render( + } + emptyState={
welcome
} + showScrollToBottomButton={false} + />, + ); + const scroller = container.querySelector(".thread-viewport-scrollbar") as HTMLElement; + const input = screen.getByLabelText("Message input"); + + act(() => { + input.focus(); + fireEvent.focusIn(input); + }); + + await waitFor(() => expect(scroller).toHaveStyle({ bottom: "320px" })); + } finally { + visualViewport.restore(); + } + }); + + it("separates the mobile welcome copy and composer into responsive rows", () => { + render( + composer
} + emptyState={
welcome
} + />, + ); + + const layout = screen.getByTestId("thread-welcome-layout"); + expect(layout).toHaveClass( + "grid", + "gap-8", + "grid-rows-[minmax(min-content,1fr)_auto]", + ); + expect(screen.getByText("welcome").parentElement).toHaveClass("min-h-0"); + expect(screen.getByText("welcome").parentElement?.className).toContain( + "sm:bottom-[calc(100%+2rem)]", + ); + }); + + it("allows the welcome view to scroll when a short viewport overflows", async () => { + const { container } = render( + composer
} + emptyState={
welcome
} + showScrollToBottomButton={false} + />, + ); + const scroller = container.querySelector(".thread-viewport-scrollbar") as HTMLElement; + Object.defineProperties(scroller, { + scrollHeight: { configurable: true, value: 620 }, + clientHeight: { configurable: true, value: 320 }, + }); + + act(() => { + window.dispatchEvent(new Event("resize")); + }); + + await waitFor(() => expect(scroller).toHaveClass("overflow-y-auto")); + }); + it("scrolls recent messages into view when the composer receives focus", async () => { const scrollTo = vi.fn(); const { container } = render(