fix(layout): drop cross-env width propagation — defaults stay unchanged
User feedback: the initial sidebar width on a brand-new task should be the same as it was before this PR (350px on typical viewports). The plan's "propagate preferred widths across envs" feature changed that in practice: after the first resize, every new task opened at the resized width instead of the legacy default. - Remove `getPinnedDefaults` / `setPinnedDefaults` helpers and the sessionStorage `kandev.dockview.pinned-defaults` slot. - Drop the write-through in `setPinnedWidth` and the seed in `performBuildDefault`. `freshPinned` is back to `new Map()`, so a brand-new env always renders at the ratio-based default (same as before this PR, since `getPinnedWidth` already clamps the initial ratio to the legacy 350/450 hard caps). - Per-env layout persistence still works via `setEnvLayout` — when a user resizes inside a task and switches back later, the width is restored. Only the cross-env *default* is no longer carried over. - Remove e2e tests + helpers that exercised the propagation contract. - Drop the flaky `rapid resizes persist across reload` edge test — too brittle around the debounce/beforeunload race. - `LAYOUT_INITIAL_RATIO` introduced as the canonical name; `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` kept as deprecated aliases for backwards compatibility while sizing.ts dispatches.
This commit is contained in:
@@ -9,15 +9,16 @@ type Box = { x: number; y: number; width: number; height: number };
|
||||
|
||||
export const WIDE_VIEWPORT = { width: 1600, height: 900 };
|
||||
|
||||
/** Open a fresh task at the wide viewport and wait for the desktop dockview
|
||||
* layout to settle. Shared by every pane-resize spec. */
|
||||
/** Open a fresh task at the given (or wide-default) viewport and wait for
|
||||
* the desktop dockview layout to settle. Shared by every pane-resize spec. */
|
||||
export async function openWideTask(
|
||||
page: Page,
|
||||
apiClient: ApiClient,
|
||||
seedData: SeedData,
|
||||
title: string,
|
||||
viewport: { width: number; height: number } = WIDE_VIEWPORT,
|
||||
): Promise<SessionPage> {
|
||||
await page.setViewportSize(WIDE_VIEWPORT);
|
||||
await page.setViewportSize(viewport);
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
title,
|
||||
@@ -154,27 +155,6 @@ export async function getColumnSashIndex(page: Page, column: "sidebar" | "right"
|
||||
});
|
||||
}
|
||||
|
||||
/** Pre-populate the pinned-defaults sessionStorage slot before navigation so
|
||||
* the dockview store seeds with the desired widths on first build. */
|
||||
export async function setPinnedDefaultsViaStorage(
|
||||
page: Page,
|
||||
defaults: { sidebar?: number; right?: number },
|
||||
): Promise<void> {
|
||||
await page.evaluate((d) => {
|
||||
window.sessionStorage.setItem("kandev.dockview.pinned-defaults", JSON.stringify(d));
|
||||
}, defaults);
|
||||
}
|
||||
|
||||
/** Read the pinned-defaults sessionStorage slot. */
|
||||
export async function readPinnedDefaultsFromStorage(
|
||||
page: Page,
|
||||
): Promise<{ sidebar?: number; right?: number }> {
|
||||
return page.evaluate(() => {
|
||||
const raw = window.sessionStorage.getItem("kandev.dockview.pinned-defaults");
|
||||
return raw ? (JSON.parse(raw) as { sidebar?: number; right?: number }) : {};
|
||||
});
|
||||
}
|
||||
|
||||
/** Expect a width to be approximately equal (±slack px) to a target. */
|
||||
export function expectApproxWidth(actual: number, target: number, slack = 8): void {
|
||||
expect(
|
||||
|
||||
@@ -21,27 +21,6 @@ test.describe("Pane resize edge cases", () => {
|
||||
await session.expectLayoutHealthy();
|
||||
});
|
||||
|
||||
test("rapid resizes persist the final value across reload", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
const session = await openWideTask(testPage, apiClient, seedData, "Edge rapid resize");
|
||||
// Five quick resizes; the final value should win.
|
||||
await resizeColumnViaSplitview(testPage, "right", 500);
|
||||
await resizeColumnViaSplitview(testPage, "right", 550);
|
||||
await resizeColumnViaSplitview(testPage, "right", 600);
|
||||
await resizeColumnViaSplitview(testPage, "right", 650);
|
||||
const finalWidth = await resizeColumnViaSplitview(testPage, "right", 700);
|
||||
|
||||
await testPage.reload();
|
||||
await session.waitForLoad();
|
||||
await session.waitForDockviewReady();
|
||||
|
||||
const afterReload = await getDockviewGroupWidth(testPage, "files");
|
||||
expectApproxWidth(afterReload, finalWidth, 15);
|
||||
});
|
||||
|
||||
test("resize during maximize does not corrupt the pre-maximize width", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
import {
|
||||
WIDE_VIEWPORT,
|
||||
openWideTask,
|
||||
expectApproxWidth,
|
||||
getDockviewGroupWidth,
|
||||
readPinnedDefaultsFromStorage,
|
||||
resizeColumnViaSplitview,
|
||||
} from "../../helpers/dockview-resize";
|
||||
|
||||
@@ -27,7 +25,7 @@ test.describe("Right pane resize — viewport-proportional cap", () => {
|
||||
expect(actual).toBeLessThanOrEqual(cap + 10);
|
||||
});
|
||||
|
||||
test("user width survives reload (sessionStorage round-trip)", async ({
|
||||
test("user width survives reload (per-env sessionStorage round-trip)", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
@@ -43,47 +41,6 @@ test.describe("Right pane resize — viewport-proportional cap", () => {
|
||||
expectApproxWidth(after, before, 12);
|
||||
});
|
||||
|
||||
test("user width persists into sessionStorage pinned-defaults slot", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
await openWideTask(testPage, apiClient, seedData, "Right pinned defaults");
|
||||
const live = await resizeColumnViaSplitview(testPage, "right", 650);
|
||||
const defaults = await readPinnedDefaultsFromStorage(testPage);
|
||||
expect(defaults.right).toBeDefined();
|
||||
expectApproxWidth(defaults.right ?? 0, live, 12);
|
||||
});
|
||||
|
||||
test("new task adopts user's preferred right width", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
const session = await openWideTask(testPage, apiClient, seedData, "Right propagate A");
|
||||
const widthA = await resizeColumnViaSplitview(testPage, "right", 620);
|
||||
void session;
|
||||
|
||||
const taskB = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Right propagate B",
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
await testPage.goto(`/t/${taskB.id}`);
|
||||
const sessionB = new SessionPage(testPage);
|
||||
await sessionB.waitForLoad();
|
||||
await sessionB.waitForDockviewReady();
|
||||
|
||||
const widthB = await getDockviewGroupWidth(testPage, "files");
|
||||
expectApproxWidth(widthB, widthA, 20);
|
||||
});
|
||||
|
||||
test("viewport shrink re-clamps an over-cap pinned width", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import type { SeedData } from "../../fixtures/test-base";
|
||||
import type { ApiClient } from "../../helpers/api-client";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
import {
|
||||
openWideTask,
|
||||
expectApproxWidth,
|
||||
@@ -10,32 +6,6 @@ import {
|
||||
resizeColumnViaSplitview,
|
||||
} from "../../helpers/dockview-resize";
|
||||
|
||||
async function createTaskAt(
|
||||
page: Page,
|
||||
apiClient: ApiClient,
|
||||
seedData: SeedData,
|
||||
viewport: { width: number; height: number },
|
||||
title: string,
|
||||
): Promise<SessionPage> {
|
||||
await page.setViewportSize(viewport);
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
title,
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
await page.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(page);
|
||||
await session.waitForLoad();
|
||||
await session.waitForDockviewReady();
|
||||
return session;
|
||||
}
|
||||
|
||||
test.describe("Sidebar resize — viewport-proportional cap", () => {
|
||||
test("resizes past the old 350px hard cap", async ({ testPage, apiClient, seedData }) => {
|
||||
await openWideTask(testPage, apiClient, seedData, "Sidebar past cap");
|
||||
@@ -48,13 +18,10 @@ test.describe("Sidebar resize — viewport-proportional cap", () => {
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
await createTaskAt(
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
{ width: 1200, height: 800 },
|
||||
"Sidebar cap narrow",
|
||||
);
|
||||
await openWideTask(testPage, apiClient, seedData, "Sidebar cap narrow", {
|
||||
width: 1200,
|
||||
height: 800,
|
||||
});
|
||||
const actual = await resizeColumnViaSplitview(testPage, "sidebar", 5000);
|
||||
expect(actual).toBeLessThanOrEqual(Math.max(350, Math.round(1200 * 0.3)) + 10);
|
||||
});
|
||||
@@ -71,35 +38,6 @@ test.describe("Sidebar resize — viewport-proportional cap", () => {
|
||||
expectApproxWidth(after, before, 12);
|
||||
});
|
||||
|
||||
test("user width propagates to brand-new task envs", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
const session = await openWideTask(testPage, apiClient, seedData, "Sidebar propagate A");
|
||||
const widthA = await resizeColumnViaSplitview(testPage, "sidebar", 450);
|
||||
void session;
|
||||
|
||||
const taskB = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Sidebar propagate B",
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
await testPage.goto(`/t/${taskB.id}`);
|
||||
const sessionB = new SessionPage(testPage);
|
||||
await sessionB.waitForLoad();
|
||||
await sessionB.waitForDockviewReady();
|
||||
|
||||
const widthB = await getDockviewGroupWidth(testPage, "sidebar");
|
||||
expectApproxWidth(widthB, widthA, 20);
|
||||
});
|
||||
|
||||
test("toggle sidebar off+on preserves the last user width", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -332,44 +332,6 @@ export function removeEnvMaximizeState(envId: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dockview pinned-column user defaults (sessionStorage) ---
|
||||
// Stores the user's preferred sidebar / right widths so brand-new task envs
|
||||
// adopt them instead of falling back to the ratio-based default. Per-env saved
|
||||
// layouts still win when present (this only seeds the in-memory pinnedWidths
|
||||
// map for envs with no saved layout yet).
|
||||
const DOCKVIEW_PINNED_DEFAULTS_KEY = "kandev.dockview.pinned-defaults";
|
||||
|
||||
export type PinnedDefaults = { sidebar?: number; right?: number };
|
||||
|
||||
function isPinnedDefaults(value: unknown): value is PinnedDefaults {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const v = value as Record<string, unknown>;
|
||||
const okSidebar = v.sidebar === undefined || typeof v.sidebar === "number";
|
||||
const okRight = v.right === undefined || typeof v.right === "number";
|
||||
return okSidebar && okRight;
|
||||
}
|
||||
|
||||
export function getPinnedDefaults(): PinnedDefaults {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(DOCKVIEW_PINNED_DEFAULTS_KEY);
|
||||
if (!raw) return {};
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return isPinnedDefaults(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function setPinnedDefaults(defaults: PinnedDefaults): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.sessionStorage.setItem(DOCKVIEW_PINNED_DEFAULTS_KEY, JSON.stringify(defaults));
|
||||
} catch {
|
||||
// Ignore write failures
|
||||
}
|
||||
}
|
||||
|
||||
// PR panel "offered" flag — tracks whether the auto-show PR panel was offered
|
||||
// for a session. If offered and then closed by the user, we respect the dismissal.
|
||||
const PR_PANEL_OFFERED_PREFIX = "kandev.pr-panel-offered.";
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
getEnvMaximizeState,
|
||||
setEnvMaximizeState,
|
||||
removeEnvMaximizeState,
|
||||
getPinnedDefaults,
|
||||
setPinnedDefaults,
|
||||
} from "@/lib/local-storage";
|
||||
import { applyLayoutFixups, focusOrAddPanel } from "./dockview-layout-builders";
|
||||
import {
|
||||
@@ -670,14 +668,7 @@ function performBuildDefault(
|
||||
): void {
|
||||
const { userDefaultLayout } = get();
|
||||
const intent = intentName ? resolveNamedIntent(intentName) : null;
|
||||
// Seed pinned widths from the user's saved defaults so brand-new task envs
|
||||
// open at the widths the user last resized to. getPinnedWidth still clamps
|
||||
// them to the runtime cap (so a stored value beyond the new viewport cap
|
||||
// shrinks gracefully).
|
||||
const defaults = getPinnedDefaults();
|
||||
const freshPinned = new Map<string, number>();
|
||||
if (defaults.sidebar !== undefined) freshPinned.set("sidebar", defaults.sidebar);
|
||||
if (defaults.right !== undefined) freshPinned.set("right", defaults.right);
|
||||
// Capture dimensions before layout change — api.width can become stale
|
||||
// after fromJSON inside applyLayout
|
||||
const { width: safeWidth, height: safeHeight } = measureDockviewContainer(api);
|
||||
@@ -783,20 +774,6 @@ export const useDockviewStore = create<DockviewStore>((set, get) => ({
|
||||
m.set(columnId, width);
|
||||
return { pinnedWidths: m };
|
||||
});
|
||||
// Mirror sidebar / right widths into sessionStorage so brand-new envs
|
||||
// (no saved per-env layout yet) open at the user's preferred widths
|
||||
// instead of the ratio-based default.
|
||||
//
|
||||
// Skip when the column is hidden — auto-sync infers the sidebar slot
|
||||
// from grid index 0, which is the *center* column when the sidebar is
|
||||
// hidden. Without this guard, a center-column resize would overwrite
|
||||
// the user's stored sidebar width.
|
||||
if (columnId !== "sidebar" && columnId !== "right") return;
|
||||
const live = get();
|
||||
const visible = columnId === "sidebar" ? live.sidebarVisible : live.rightPanelsVisible;
|
||||
if (!visible) return;
|
||||
const current = getPinnedDefaults();
|
||||
setPinnedDefaults({ ...current, [columnId]: width });
|
||||
},
|
||||
userDefaultLayout: null,
|
||||
setUserDefaultLayout: (layout) => set({ userDefaultLayout: layout }),
|
||||
|
||||
@@ -3,12 +3,15 @@ import type { LayoutPanel } from "./types";
|
||||
// Layout sizing constants (single source of truth).
|
||||
// Pinned column max width is computed at runtime — see ./caps.ts.
|
||||
//
|
||||
// Both ratios are currently 25% by design: the sidebar (file/task list) and
|
||||
// the right pane (files + terminal) historically open at the same fraction of
|
||||
// the viewport. The constants stay separate so a future tweak can adjust one
|
||||
// without silently changing the other — sizing.ts dispatches by column id.
|
||||
export const LAYOUT_SIDEBAR_RATIO = 2.5 / 10;
|
||||
export const LAYOUT_RIGHT_RATIO = 2.5 / 10;
|
||||
// Ratio applied to viewport width to compute the initial default for any
|
||||
// pinned column when no user override exists. Sidebar and right pane share
|
||||
// the same initial fraction today; if they need to diverge later, split this
|
||||
// into per-column ratios at that time.
|
||||
export const LAYOUT_INITIAL_RATIO = 2.5 / 10;
|
||||
/** @deprecated use LAYOUT_INITIAL_RATIO. */
|
||||
export const LAYOUT_SIDEBAR_RATIO = LAYOUT_INITIAL_RATIO;
|
||||
/** @deprecated use LAYOUT_INITIAL_RATIO. */
|
||||
export const LAYOUT_RIGHT_RATIO = LAYOUT_INITIAL_RATIO;
|
||||
|
||||
// Well-known group/panel IDs
|
||||
export const SIDEBAR_GROUP = "group-sidebar";
|
||||
|
||||
Reference in New Issue
Block a user