diff --git a/apps/web/components/task/dockview-layout-restore.test.ts b/apps/web/components/task/dockview-layout-restore.test.ts index d7ae3f2d2f..d0d4282517 100644 --- a/apps/web/components/task/dockview-layout-restore.test.ts +++ b/apps/web/components/task/dockview-layout-restore.test.ts @@ -284,9 +284,40 @@ describe("sanitizeLayout - session panel handling", () => { }); }); -describe("tryRestoreLayout - phantom session panel filtering", () => { +describe("tryRestoreLayout - env restore", () => { beforeEach(() => { vi.restoreAllMocks(); + document.body.replaceChildren(); + }); + + it("reconciles a stale saved grid width with the live container before applying fixups", () => { + const layout = buildLayout({ centerSize: 650, rightSize: 900 }); + layout.grid.width = 1900; + vi.spyOn(localStorage, "getEnvLayout").mockReturnValue(layout); + vi.spyOn(localStorage, "getEnvMaximizeState").mockReturnValue(null); + + const container = document.createElement("div"); + const dockview = document.createElement("div"); + dockview.className = "dv-dockview"; + container.appendChild(dockview); + document.body.appendChild(container); + Object.defineProperties(container, { + clientWidth: { value: 1260 }, + clientHeight: { value: 700 }, + }); + + const api = makeFakeRestoreApi(); + Object.defineProperties(api, { + width: { value: 1900, writable: true }, + height: { value: 600, writable: true }, + }); + + expect(tryRestoreLayout(api, "env-stale-width", VALID_COMPONENTS)).toBe(true); + expect(api.fromJSON).toHaveBeenCalledOnce(); + expect(api.layout).toHaveBeenCalledWith(1260, 700); + expect((api.fromJSON as ReturnType).mock.invocationCallOrder[0]).toBeLessThan( + (api.layout as ReturnType).mock.invocationCallOrder[0], + ); }); it("restores the (stripped) layout when every session in the saved env-layout is a phantom", () => { diff --git a/apps/web/components/task/dockview-layout-restore.ts b/apps/web/components/task/dockview-layout-restore.ts index d4877da387..43ca85c8e9 100644 --- a/apps/web/components/task/dockview-layout-restore.ts +++ b/apps/web/components/task/dockview-layout-restore.ts @@ -185,6 +185,8 @@ function applyFixupsWithMaximize( if (savedMax) { applySavedMaximize(api, savedMax, savedRightWidth); } else { + const { width, height } = measureDockviewContainer(api); + api.layout(width, height); // Anchor the right column to its per-env saved width (see // `captureRightTarget`) so a page reload restores the task's remembered // width instead of resetting it to the default. diff --git a/apps/web/e2e/tests/layout/pane-resize-right.spec.ts b/apps/web/e2e/tests/layout/pane-resize-right.spec.ts index 0b925c85a9..e1f2abab45 100644 --- a/apps/web/e2e/tests/layout/pane-resize-right.spec.ts +++ b/apps/web/e2e/tests/layout/pane-resize-right.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "../../fixtures/test-base"; import { computeRightMaxPx } from "../../../lib/state/layout-manager/caps"; +import { TERMINAL_DEFAULT_ID } from "../../../lib/state/layout-manager/constants"; import { WIDE_VIEWPORT, openWideTask, @@ -48,6 +49,73 @@ test.describe("Right pane resize — container-proportional cap", () => { expectApproxWidth(after, before, 12); }); + test("initial restore reconciles a stale saved grid width without a manual resize", async ({ + testPage, + apiClient, + seedData, + }) => { + await testPage.setViewportSize({ width: 2200, height: 900 }); + const task = await apiClient.createTaskWithAgent( + seedData.workspaceId, + "Right stale grid restore", + seedData.agentProfileId, + { + description: "/e2e:simple-message", + workflow_id: seedData.workflowId, + workflow_step_id: seedData.startStepId, + repository_ids: [seedData.repositoryId], + }, + ); + if (!task.session_id) throw new Error("created task did not return a session_id"); + await testPage.goto(`/t/${task.id}`); + const session = new SessionPage(testPage); + await session.waitForLoad(); + await session.waitForDockviewReady(); + expect(await resizeColumnViaSplitview(testPage, "right", 900)).toBeGreaterThan(800); + + const staleLayout = await testPage.evaluate((sessionId) => { + type StoreWindow = Window & { + __KANDEV_E2E_STORE__?: { + getState: () => { environmentIdBySessionId: Record }; + }; + }; + const store = (window as StoreWindow).__KANDEV_E2E_STORE__; + const envId = store?.getState().environmentIdBySessionId[sessionId]; + if (!envId) throw new Error("environment id not hydrated"); + const key = `kandev.dockview.env-layout-v3.${envId}`; + const raw = window.sessionStorage.getItem(key); + if (!raw) throw new Error("env layout was not persisted"); + const layout = JSON.parse(raw); + const columns = layout.grid?.root?.data; + if (!Array.isArray(columns) || columns.length < 2) { + throw new Error("saved layout does not contain center and right columns"); + } + layout.grid.width = 1900; + columns[0].size = 1000; + columns[columns.length - 1].size = 900; + return { key, json: JSON.stringify(layout) }; + }, task.session_id); + + await testPage.addInitScript(({ key, json }) => { + window.sessionStorage.setItem(key, json); + }, staleLayout); + await testPage.setViewportSize({ width: 1540, height: 900 }); + await testPage.reload(); + await session.waitForLoad(); + await session.waitForDockviewReady(); + + const dockviewBox = await testPage.locator(".dv-dockview").boundingBox(); + expect(dockviewBox).not.toBeNull(); + const rightWidth = await getDockviewGroupWidth(testPage, "files"); + const terminalWidth = await getDockviewGroupWidth(testPage, TERMINAL_DEFAULT_ID); + const centerWidth = await getDockviewGroupWidth(testPage, `session:${task.session_id}`); + const cap = computeRightMaxPx(dockviewBox?.width ?? 0); + expect(rightWidth).toBeLessThanOrEqual(cap + 10); + expectApproxWidth(terminalWidth, rightWidth, 10); + // Allow 2px of rendering tolerance around the 480px center comfort reserve. + expect(centerWidth).toBeGreaterThanOrEqual(478); + }); + test("viewport shrink re-clamps an over-cap pinned width", async ({ testPage, apiClient,