fix(webui): anchor sent prompts during active turns

This commit is contained in:
Xubin Ren
2026-06-22 20:43:27 +08:00
parent fbaa85117b
commit 7170761e47
9 changed files with 333 additions and 56 deletions
+67 -2
View File
@@ -110,7 +110,7 @@ function ViewportWithPromptNavigator({ messages }: { messages: UIMessage[] }) {
}
describe("ThreadViewport", () => {
it("bottom-aligns short history near the composer", () => {
it("top-aligns short threads in the message rendering area", () => {
render(
<ThreadViewport
messages={messages}
@@ -120,11 +120,76 @@ describe("ThreadViewport", () => {
);
const messageRegion = screen.getByTestId("thread-message-region");
expect(messageRegion).toHaveClass("justify-end");
expect(messageRegion).toHaveClass("justify-start");
expect(messageRegion).not.toHaveClass("justify-end");
expect(messageRegion).toHaveClass("pb-4");
expect(messageRegion.className).not.toContain("5rem");
});
it("top-aligns a short active turn while the agent is responding", () => {
render(
<ThreadViewport
messages={messages}
isStreaming
composer={<div>composer</div>}
/>,
);
const messageRegion = screen.getByTestId("thread-message-region");
expect(messageRegion).toHaveClass("justify-start");
expect(messageRegion).not.toHaveClass("justify-end");
expect(screen.getByTestId("thread-composer-dock")).not.toHaveClass("mt-auto");
});
it("anchors the latest user prompt after sending instead of scrolling to the bottom", async () => {
const threaded: UIMessage[] = [
{ id: "u1", role: "user", content: "old question", createdAt: 1 },
{ id: "a1", role: "assistant", content: "old answer", createdAt: 2 },
{ id: "u2", role: "user", content: "new question", createdAt: 3 },
];
const scrollTo = vi.fn();
const { container, rerender } = render(
<ThreadViewport
messages={threaded}
isStreaming
composer={<div>composer</div>}
scrollToLatestUserPromptSignal={0}
/>,
);
const scroller = container.firstElementChild?.firstElementChild as HTMLElement;
Object.defineProperties(scroller, {
scrollHeight: { configurable: true, value: 1200 },
clientHeight: { configurable: true, value: 500 },
scrollTop: { configurable: true, writable: true, value: 700 },
scrollTo: { configurable: true, value: scrollTo },
});
const prompt = container.querySelector<HTMLElement>('[data-user-prompt-id="u2"]');
expect(prompt).not.toBeNull();
Object.defineProperty(prompt, "offsetTop", {
configurable: true,
value: 420,
});
scrollTo.mockClear();
await act(async () => {
rerender(
<ThreadViewport
messages={threaded}
isStreaming
composer={<div>composer</div>}
scrollToLatestUserPromptSignal={1}
/>,
);
});
expect(scrollTo).toHaveBeenCalledWith({
top: 404,
behavior: "auto",
});
expect(screen.getByTestId("thread-message-region")).toHaveClass("justify-start");
});
it("keeps the scroll-to-bottom button above a growing composer", () => {
const originalResizeObserver = globalThis.ResizeObserver;
const resizeObservers: ResizeObserverInstance[] = [];