From 2ed65cfde3ac3b6f6acabb026741d699d6a4c874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:35:29 +0100 Subject: [PATCH] fix(web): guard dockview measure against mid-transition sidebar width (#1288) * fix(web): guard dockview measure against mid-transition sidebar width The unified AppSidebar (#1165) animates its width over 300ms; the dockview content container (a flex sibling) can be measured mid-transition at a tiny positive width in onReady, so the default layout builds too narrow and dockview collapses the horizontal columns into a vertical stack. measureDockviewContainer trusted any positive parent clientWidth. Reject an implausibly-small live width (< 50% of viewport) as a not-yet-laid-out transient and fall back to the viewport; the resize observer then snaps to the exact size. * fix(web): preserve live height when width guard fires; assert it --- apps/web/lib/state/dockview-measure.test.ts | 23 +++++++++++++++++++++ apps/web/lib/state/dockview-measure.ts | 19 ++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/state/dockview-measure.test.ts b/apps/web/lib/state/dockview-measure.test.ts index d95f2b9f7c..8448e8fc3a 100644 --- a/apps/web/lib/state/dockview-measure.test.ts +++ b/apps/web/lib/state/dockview-measure.test.ts @@ -23,6 +23,29 @@ describe("measureDockviewContainer", () => { expect(measureDockviewContainer(fakeApi(0, 0))).toEqual({ width: 1500, height: 700 }); }); + it("ignores an implausibly-narrow live width (sidebar mid-transition) but keeps the live height", () => { + // Regression (post unified-sidebar #1165): the AppSidebar is a root-layout + // flex sibling with `transition-all duration-300`, so the dockview content + // container can be measured mid-animation at a tiny positive width. Building + // the default layout at that width collapses the horizontal columns into a + // vertical stack (chat / files+changes / terminal). On desktop the sidebar + // maxes at 30vw, so content is always >= ~70vw — a sub-half-viewport read is + // definitionally a transient: fall back the width to the viewport so the + // build stays horizontal (the resize observer then snaps to the exact size). + // The animation is horizontal, so the live height is reliable and is kept. + const parent = document.createElement("div"); + Object.defineProperty(parent, "clientWidth", { value: 80, configurable: true }); + Object.defineProperty(parent, "clientHeight", { value: 700, configurable: true }); + const dv = document.createElement("div"); + dv.className = "dv-dockview"; + parent.appendChild(dv); + document.body.appendChild(parent); + + const result = measureDockviewContainer(fakeApi(0, 0)); + expect(result.width).toBe(window.innerWidth); + expect(result.height).toBe(700); + }); + it("never returns a zero size on a fresh mount (no container, api not laid out yet)", () => { // Regression: a 0×0 measurement builds the default layout at zero width, so // dockview collapses the horizontal columns into a vertical stack (chat / diff --git a/apps/web/lib/state/dockview-measure.ts b/apps/web/lib/state/dockview-measure.ts index 3b050ab4fd..8ba3e92ea9 100644 --- a/apps/web/lib/state/dockview-measure.ts +++ b/apps/web/lib/state/dockview-measure.ts @@ -25,12 +25,29 @@ export function measureDockviewContainer(api: DockviewApi): { width: number; hei }; } +// The dockview content area sits next to the unified AppSidebar (#1165), whose +// width animates over 300ms (`transition-all`). During that animation `onReady` +// can read the parent at a tiny positive width — building the default layout at +// that width collapses the horizontal columns into a vertical stack. The +// sidebar maxes at 30vw (desktop-only surface), so a healthy content width is +// always >= ~70vw; anything below half the viewport is a mid-transition +// transient and must be treated like a not-yet-laid-out (0-width) container. +const MIN_PLAUSIBLE_WIDTH_RATIO = 0.5; + function liveContainerSize(): { width: number; height: number } | null { if (typeof document === "undefined") return null; const dv = document.querySelector(".dv-dockview") as HTMLElement | null; const parent = dv?.parentElement; if (!parent || parent.clientWidth <= 0 || parent.clientHeight <= 0) return null; - return { width: parent.clientWidth, height: parent.clientHeight }; + // The sidebar animation is horizontal, so only the width can be a transient — + // the live height is reliable. Fall back just the width to the viewport so the + // build stays horizontal; keep the real height to avoid a needless vertical + // transient. The resize observer then snaps the width to the exact size. + const plausibleWidth = parent.clientWidth >= viewportWidth() * MIN_PLAUSIBLE_WIDTH_RATIO; + return { + width: plausibleWidth ? parent.clientWidth : viewportWidth(), + height: parent.clientHeight, + }; } function viewportWidth(): number {