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:
José Almeida
2026-05-22 20:37:40 +01:00
parent 4e393b2448
commit fb3dbe4c50
7 changed files with 18 additions and 222 deletions
+4 -24
View File
@@ -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,
-38
View File
@@ -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.";
-23
View File
@@ -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";