From c7c22b9d7e8d33886fdccff172d2b38c4e155460 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 24 Aug 2026 01:59:00 +0800 Subject: [PATCH] fix(app): stabilize pane transitions (#44484) --- .../e2e/regression/terminal-hidden.spec.ts | 63 +++++++++++++++++-- packages/app/src/session/helpers.ts | 4 +- packages/app/src/session/screen.tsx | 3 +- .../app/src/session/terminal/terminal.tsx | 6 +- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index a66f6655909..84e3c91ddd1 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -131,8 +131,11 @@ test("animates review and terminal panels while caching hidden terminal content" await resetTerminalBottomMotion(page) await resetTerminalAnchorGaps(page) await resetPanelGaps(page) + const reviewContent = page.locator('[data-component="session-review-v2"]') + await reviewContent.evaluate((element) => element.setAttribute("data-cache-probe", "original")) await reviewToggle.click() - await expect(page.locator("#review-panel")).toHaveCount(0) + await expect(page.locator("#review-panel")).toBeHidden() + await expect(reviewContent).toHaveAttribute("data-cache-probe", "original") await expect(panel).toBeVisible() await expectHeightMotions(page, "session-side-region", 2) await expectHeightMotions(page, "session-side-terminal-region", 2) @@ -142,6 +145,7 @@ test("animates review and terminal panels while caching hidden terminal content" await expectPanelGapHeld(page) await reviewToggle.click() await expect(page.locator("#review-panel")).toBeVisible() + await expect(reviewContent).toHaveAttribute("data-cache-probe", "original") await expectHeightMotions(page, "session-side-region", 3) await expectHeightMotions(page, "session-side-terminal-region", 3) @@ -161,14 +165,18 @@ test("animates review and terminal panels while caching hidden terminal content" await reviewToggle.click() await expect(page.locator("#review-panel")).toHaveCount(0) await expectWidthMotions(page, 2) + await expectSideSlideSettled(page, 2) + await expectHiddenSideAligned(page) await resetHeightMotions(page) + await resetHorizontalScrolls(page) await page.keyboard.press("Control+Backquote") await expect(panel).toHaveAttribute("aria-hidden", "false") await expect(page.locator('[data-component="terminal"]')).toBeVisible() await expectWidthMotions(page, 3) - await expectSideMotionSettled(page) + await expectSideSlideSettled(page, 3) await expectNoHeightMotion(page) + await expectNoHorizontalScroll(page) await page.keyboard.press("Control+Backquote") await expect(panel).toBeHidden() @@ -209,6 +217,8 @@ test("animates review and terminal panels while caching hidden terminal content" type MotionProbe = { widths: number + widthEnds: number + horizontalScrolls: number[] reviewWidths: number[] paintGaps: { review: number; terminalSurface: number }[] terminalContentSizes: { width: number; height: number }[] @@ -225,6 +235,8 @@ async function installMotionProbe(page: Page) { await page.evaluate(() => { const probe: MotionProbe = { widths: 0, + widthEnds: 0, + horizontalScrolls: [], reviewWidths: [], paintGaps: [], terminalContentSizes: [], @@ -299,10 +311,16 @@ async function installMotionProbe(page: Page) { probe.heights.push(slot) } }) + document.addEventListener("transitionend", (event) => { + if (!(event.target instanceof Element)) return + if (event.propertyName === "width" && event.target.getAttribute("data-slot") === "session-chat-panel") + probe.widthEnds++ + }) document.addEventListener("animationstart", (event) => { if (!(event.target instanceof Element) || event.target.getAttribute("data-component") !== "terminal-panel") return probe.animations.push(event.animationName) }) + window.addEventListener("scroll", () => probe.horizontalScrolls.push(window.scrollX)) ;(window as Window & { __panelMotion?: MotionProbe }).__panelMotion = probe }) } @@ -320,11 +338,10 @@ async function resetHeightMotions(page: Page) { }) } -async function expectSideMotionSettled(page: Page) { - const side = page.locator('[data-slot="session-side-panel-presence"]') +async function expectSideSlideSettled(page: Page, count: number) { await expect - .poll(() => side.evaluate((element) => element.getAnimations().every((item) => item.playState === "finished"))) - .toBe(true) + .poll(() => page.evaluate(() => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.widthEnds ?? 0)) + .toBeGreaterThanOrEqual(count) } async function expectNoHeightMotion(page: Page) { @@ -334,6 +351,40 @@ async function expectNoHeightMotion(page: Page) { expect(heights).toEqual([]) } +async function expectHiddenSideAligned(page: Page) { + await expect + .poll(() => + page.evaluate(() => { + const chat = document.querySelector('[data-slot="session-chat-panel"]') + const side = document.querySelector('[data-slot="session-side-panel-presence"]') + if (!chat?.parentElement || !side) return Number.POSITIVE_INFINITY + const row = chat.parentElement.getBoundingClientRect() + const hidden = side.getBoundingClientRect() + return Math.max( + Math.abs(row.top - hidden.top), + Math.abs(row.right - hidden.right), + Math.abs(row.bottom - hidden.bottom), + ) + }), + ) + .toBeLessThanOrEqual(1) +} + +async function resetHorizontalScrolls(page: Page) { + await page.evaluate(() => { + const probe = (window as Window & { __panelMotion?: MotionProbe }).__panelMotion + if (probe) probe.horizontalScrolls = [] + }) +} + +async function expectNoHorizontalScroll(page: Page) { + const scrolls = await page.evaluate( + () => (window as Window & { __panelMotion?: MotionProbe }).__panelMotion?.horizontalScrolls ?? [], + ) + expect(Math.max(0, ...scrolls)).toBe(0) + expect(await page.evaluate(() => window.scrollX)).toBe(0) +} + async function expectReviewWidthStable(page: Page) { const side = page.locator('[data-slot="session-side-panel-presence"]') await expect diff --git a/packages/app/src/session/helpers.ts b/packages/app/src/session/helpers.ts index ff6e334adf9..73a06f1cb2a 100644 --- a/packages/app/src/session/helpers.ts +++ b/packages/app/src/session/helpers.ts @@ -101,11 +101,11 @@ export const focusTerminalById = (id: string) => { const textarea = terminal.querySelector("textarea") if (textarea instanceof HTMLTextAreaElement) { - textarea.focus() + textarea.focus({ preventScroll: true }) return true } - terminal.focus() + terminal.focus({ preventScroll: true }) terminal.dispatchEvent( typeof PointerEvent === "function" ? new PointerEvent("pointerdown", { bubbles: true, cancelable: true }) diff --git a/packages/app/src/session/screen.tsx b/packages/app/src/session/screen.tsx index e5c56685fe9..3d640e76af1 100644 --- a/packages/app/src/session/screen.tsx +++ b/packages/app/src/session/screen.tsx @@ -165,7 +165,7 @@ export function SessionScreen(props: { session: SessionModel }) { <>
-
+
{ const focusTerminal = () => { const t = term if (!t) return - t.focus() - t.textarea?.focus() - setTimeout(() => t.textarea?.focus(), 0) + const focus = () => (t.textarea ? t.textarea.focus({ preventScroll: true }) : t.focus()) + focus() + setTimeout(focus, 0) } const handlePointerDown = () => { const activeElement = document.activeElement