diff --git a/packages/app/e2e/regression/timeline-geometry.spec.ts b/packages/app/e2e/regression/timeline-geometry.spec.ts index f5990a4fd69a..8f90e659574c 100644 --- a/packages/app/e2e/regression/timeline-geometry.spec.ts +++ b/packages/app/e2e/regression/timeline-geometry.spec.ts @@ -630,6 +630,37 @@ test("keeps a reader scrolled up in place when they submit a prompt", async ({ p await expect.poll(fromEnd).toBeLessThanOrEqual(1) }) +test("takes a reader scrolled up short of the jump button to their submitted prompt", async ({ page }) => { + await setupTimeline(page, { + reducedMotion: true, + sessionMessages: [ + ...validateTimelineMessages([...historyMessages(18), userMessage()]), + { id: "msg_1002_timeline_idle", type: "idle", outcome: "succeeded", time: { created: 1700000003000 } }, + ], + }) + const scroller = page.locator('[data-slot="session-timeline-scroll"] > [data-scrollable]') + const fromEnd = () => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop) + const jump = page.getByRole("button", { name: "Jump to latest" }) + const editor = page.locator('[data-component="composer"]').getByRole("textbox") + await expect(page.locator("[data-timeline-virtual-content]")).toHaveCSS("visibility", "visible") + await expect.poll(fromEnd).toBeLessThanOrEqual(1) + + await scroller.hover() + + // Back-to-back prompts, each sent after a small scroll up, bring the reader down to the one just sent. + for (const text of ["First prompt sent just above the end.", "Second prompt sent right after."]) { + await page.mouse.wheel(0, -150) + await expect.poll(fromEnd).toBeGreaterThan(100) + // The hidden button only fades out, so its inert wrapper is what says it is not offered. + await expect(jump.locator("xpath=..")).toHaveAttribute("inert") + await editor.fill(text) + await editor.press("Enter") + await expect(editor).toHaveText("") + await expect(page.locator('[data-timeline-row="UserMessage"]').filter({ hasText: text })).toBeInViewport() + await expect.poll(fromEnd).toBeLessThanOrEqual(1) + } +}) + async function captureCardEdges(page: Page, card: Locator) { const box = await card.boundingBox() diff --git a/packages/app/src/session/composer/region.tsx b/packages/app/src/session/composer/region.tsx index 28a710e04f31..d1432a66e1f9 100644 --- a/packages/app/src/session/composer/region.tsx +++ b/packages/app/src/session/composer/region.tsx @@ -338,11 +338,11 @@ export function createActiveSessionRegion(input: { state, parentID: input.session.data.parentID, centered: input.screen.centered, - // Submitting reads past the automatic unread marker. Keep a reader's own scroll or revealed tool in place. + // A reader far enough up to see the jump-to-latest button stays put; anyone closer is taken to the new prompt. onResponseSubmit: () => { - const anchor = input.timeline.follow.anchor() + const scroll = input.timeline.scroll - if (input.timeline.follow.following() || (anchor && !anchor.partID)) input.timeline.actions.resume() + if (input.timeline.follow.following() || !(scroll.overflow && scroll.jump)) input.timeline.actions.resume() }, openParent, focusPrompt: () => focusPrompt(),